From dd0ad4c2459e5bb56a5b8fa646c3db0d2e8c8c07 Mon Sep 17 00:00:00 2001 From: Aine Date: Sun, 11 Feb 2024 20:47:04 +0200 Subject: [PATCH] refactor to mautrix 0.17.x; update deps --- bot/access.go | 78 +- bot/activation.go | 13 +- bot/bot.go | 17 +- bot/command.go | 44 +- bot/command_admin.go | 118 +-- bot/command_owner.go | 60 +- bot/config/manager.go | 38 +- bot/context.go | 6 +- bot/data.go | 33 +- bot/email.go | 112 +-- bot/mutex.go | 10 +- bot/queue/manager.go | 11 +- bot/queue/queue.go | 23 +- bot/reaction.go | 6 +- bot/sync.go | 35 +- cmd/cmd.go | 3 +- config/config.go | 5 + config/types.go | 9 + email/email.go | 13 +- go.mod | 20 +- go.sum | 44 +- smtp/listener.go | 7 +- smtp/manager.go | 16 +- smtp/server.go | 14 +- smtp/session.go | 22 +- utils/psd.go | 112 +++ vendor/github.com/rs/zerolog/README.md | 2 +- vendor/github.com/rs/zerolog/console.go | 66 +- vendor/github.com/rs/zerolog/context.go | 33 +- vendor/github.com/rs/zerolog/event.go | 2 +- vendor/github.com/rs/zerolog/example.jsonl | 7 + vendor/github.com/rs/zerolog/fields.go | 23 +- vendor/github.com/rs/zerolog/globals.go | 28 + vendor/github.com/rs/zerolog/log.go | 32 +- vendor/github.com/rs/zerolog/pretty.png | Bin 84064 -> 118839 bytes vendor/github.com/rs/zerolog/syslog.go | 9 + vendor/github.com/rs/zerolog/writer.go | 164 ++++ vendor/github.com/yuin/goldmark/README.md | 34 +- .../yuin/goldmark/extension/footnote.go | 24 +- .../yuin/goldmark/extension/linkify.go | 8 +- .../yuin/goldmark/extension/typographer.go | 4 +- .../yuin/goldmark/parser/html_block.go | 2 +- .../yuin/goldmark/parser/raw_html.go | 39 +- vendor/github.com/yuin/goldmark/util/util.go | 2 +- vendor/gitlab.com/etke.cc/go/env/LICENSE | 817 ++++-------------- vendor/gitlab.com/etke.cc/go/env/env.go | 26 +- .../etke.cc/linkpearl/accountdata.go | 17 +- vendor/gitlab.com/etke.cc/linkpearl/config.go | 3 +- vendor/gitlab.com/etke.cc/linkpearl/events.go | 25 +- .../gitlab.com/etke.cc/linkpearl/linkpearl.go | 17 +- vendor/gitlab.com/etke.cc/linkpearl/send.go | 20 +- vendor/gitlab.com/etke.cc/linkpearl/sync.go | 53 +- vendor/go.mau.fi/util/dbutil/connlog.go | 70 +- vendor/go.mau.fi/util/dbutil/database.go | 48 +- vendor/go.mau.fi/util/dbutil/iter.go | 72 ++ vendor/go.mau.fi/util/dbutil/log.go | 14 +- vendor/go.mau.fi/util/dbutil/queryhelper.go | 105 +++ vendor/go.mau.fi/util/dbutil/transaction.go | 22 +- vendor/go.mau.fi/util/dbutil/upgrades.go | 101 +-- vendor/go.mau.fi/util/dbutil/upgradetable.go | 15 +- vendor/go.mau.fi/util/exerrors/must.go | 23 + vendor/go.mau.fi/util/jsontime/integer.go | 73 ++ .../golang.org/x/crypto/argon2/blamka_amd64.s | 12 +- .../x/crypto/blake2b/blake2bAVX2_amd64.go | 2 +- .../x/crypto/blake2b/blake2bAVX2_amd64.s | 2 +- .../x/crypto/blake2b/blake2b_amd64.go | 24 - .../golang.org/x/crypto/blake2b/register.go | 2 - .../x/crypto/internal/poly1305/bits_compat.go | 39 - .../x/crypto/internal/poly1305/bits_go1.13.go | 21 - .../x/crypto/internal/poly1305/sum_generic.go | 43 +- vendor/golang.org/x/crypto/ssh/channel.go | 28 +- vendor/golang.org/x/crypto/ssh/client.go | 2 +- vendor/golang.org/x/crypto/ssh/client_auth.go | 20 +- vendor/golang.org/x/crypto/ssh/common.go | 8 + vendor/golang.org/x/crypto/ssh/handshake.go | 56 +- vendor/golang.org/x/crypto/ssh/server.go | 7 +- vendor/golang.org/x/crypto/ssh/tcpip.go | 35 + vendor/golang.org/x/crypto/ssh/transport.go | 32 +- vendor/golang.org/x/exp/slices/slices.go | 50 +- vendor/golang.org/x/net/html/token.go | 12 +- vendor/golang.org/x/sys/unix/fcntl.go | 2 +- vendor/golang.org/x/sys/unix/ioctl_linux.go | 5 + vendor/golang.org/x/sys/unix/mkerrors.sh | 42 +- vendor/golang.org/x/sys/unix/syscall_bsd.go | 2 +- vendor/golang.org/x/sys/unix/syscall_linux.go | 26 +- .../golang.org/x/sys/unix/syscall_openbsd.go | 14 + .../golang.org/x/sys/unix/syscall_solaris.go | 2 +- .../x/sys/unix/syscall_zos_s390x.go | 2 +- vendor/golang.org/x/sys/unix/zerrors_linux.go | 92 +- .../x/sys/unix/zerrors_linux_386.go | 3 + .../x/sys/unix/zerrors_linux_amd64.go | 3 + .../x/sys/unix/zerrors_linux_arm.go | 3 + .../x/sys/unix/zerrors_linux_arm64.go | 3 + .../x/sys/unix/zerrors_linux_loong64.go | 3 + .../x/sys/unix/zerrors_linux_mips.go | 3 + .../x/sys/unix/zerrors_linux_mips64.go | 3 + .../x/sys/unix/zerrors_linux_mips64le.go | 3 + .../x/sys/unix/zerrors_linux_mipsle.go | 3 + .../x/sys/unix/zerrors_linux_ppc.go | 3 + .../x/sys/unix/zerrors_linux_ppc64.go | 3 + .../x/sys/unix/zerrors_linux_ppc64le.go | 3 + .../x/sys/unix/zerrors_linux_riscv64.go | 3 + .../x/sys/unix/zerrors_linux_s390x.go | 3 + .../x/sys/unix/zerrors_linux_sparc64.go | 3 + .../golang.org/x/sys/unix/zsyscall_linux.go | 15 + .../x/sys/unix/zsyscall_openbsd_386.go | 28 +- .../x/sys/unix/zsyscall_openbsd_386.s | 5 + .../x/sys/unix/zsyscall_openbsd_amd64.go | 28 +- .../x/sys/unix/zsyscall_openbsd_amd64.s | 5 + .../x/sys/unix/zsyscall_openbsd_arm.go | 28 +- .../x/sys/unix/zsyscall_openbsd_arm.s | 5 + .../x/sys/unix/zsyscall_openbsd_arm64.go | 28 +- .../x/sys/unix/zsyscall_openbsd_arm64.s | 5 + .../x/sys/unix/zsyscall_openbsd_mips64.go | 28 +- .../x/sys/unix/zsyscall_openbsd_mips64.s | 5 + .../x/sys/unix/zsyscall_openbsd_ppc64.go | 28 +- .../x/sys/unix/zsyscall_openbsd_ppc64.s | 6 + .../x/sys/unix/zsyscall_openbsd_riscv64.go | 28 +- .../x/sys/unix/zsyscall_openbsd_riscv64.s | 5 + .../x/sys/unix/zsysnum_linux_386.go | 4 + .../x/sys/unix/zsysnum_linux_amd64.go | 3 + .../x/sys/unix/zsysnum_linux_arm.go | 4 + .../x/sys/unix/zsysnum_linux_arm64.go | 4 + .../x/sys/unix/zsysnum_linux_loong64.go | 4 + .../x/sys/unix/zsysnum_linux_mips.go | 4 + .../x/sys/unix/zsysnum_linux_mips64.go | 4 + .../x/sys/unix/zsysnum_linux_mips64le.go | 4 + .../x/sys/unix/zsysnum_linux_mipsle.go | 4 + .../x/sys/unix/zsysnum_linux_ppc.go | 4 + .../x/sys/unix/zsysnum_linux_ppc64.go | 4 + .../x/sys/unix/zsysnum_linux_ppc64le.go | 4 + .../x/sys/unix/zsysnum_linux_riscv64.go | 4 + .../x/sys/unix/zsysnum_linux_s390x.go | 4 + .../x/sys/unix/zsysnum_linux_sparc64.go | 4 + vendor/golang.org/x/sys/unix/ztypes_linux.go | 157 ++-- .../golang.org/x/sys/windows/env_windows.go | 17 +- .../x/sys/windows/syscall_windows.go | 6 +- .../x/sys/windows/zsyscall_windows.go | 28 + .../go/mautrix/.pre-commit-config.yaml | 4 + vendor/maunium.net/go/mautrix/CHANGELOG.md | 24 + vendor/maunium.net/go/mautrix/README.md | 5 - vendor/maunium.net/go/mautrix/client.go | 642 +++++++------- .../maunium.net/go/mautrix/crypto/account.go | 2 +- .../go/mautrix/crypto/canonicaljson/LICENSE | 177 ---- .../go/mautrix/crypto/canonicaljson/README.md | 2 + .../go/mautrix/crypto/cross_sign_key.go | 5 +- .../go/mautrix/crypto/cross_sign_pubkey.go | 13 +- .../go/mautrix/crypto/cross_sign_signing.go | 35 +- .../go/mautrix/crypto/cross_sign_ssss.go | 31 +- .../go/mautrix/crypto/cross_sign_store.go | 10 +- .../mautrix/crypto/cross_sign_validation.go | 16 +- .../crypto/cryptohelper/cryptohelper.go | 69 +- .../go/mautrix/crypto/decryptmegolm.go | 12 +- .../go/mautrix/crypto/decryptolm.go | 14 +- .../go/mautrix/crypto/devicelist.go | 60 +- .../go/mautrix/crypto/encryptmegolm.go | 44 +- .../go/mautrix/crypto/encryptolm.go | 14 +- .../go/mautrix/crypto/goolm/README.md | 5 + .../mautrix/crypto/goolm/account/account.go | 522 +++++++++++ .../go/mautrix/crypto/goolm/base64.go | 22 + .../mautrix/crypto/goolm/cipher/aes_sha256.go | 96 ++ .../go/mautrix/crypto/goolm/cipher/main.go | 17 + .../go/mautrix/crypto/goolm/cipher/pickle.go | 58 ++ .../go/mautrix/crypto/goolm/crypto/aes_cbc.go | 75 ++ .../mautrix/crypto/goolm/crypto/curve25519.go | 186 ++++ .../go/mautrix/crypto/goolm/crypto/ed25519.go | 181 ++++ .../go/mautrix/crypto/goolm/crypto/hmac.go | 29 + .../go/mautrix/crypto/goolm/crypto/main.go | 2 + .../crypto/goolm/crypto/one_time_key.go | 95 ++ .../go/mautrix/crypto/goolm/errors.go | 30 + .../crypto/goolm/libolmpickle/pickle.go | 41 + .../crypto/goolm/libolmpickle/unpickle.go | 53 ++ .../go/mautrix/crypto/goolm/main.go | 6 + .../go/mautrix/crypto/goolm/megolm/megolm.go | 234 +++++ .../mautrix/crypto/goolm/message/decoder.go | 70 ++ .../crypto/goolm/message/group_message.go | 144 +++ .../mautrix/crypto/goolm/message/message.go | 129 +++ .../crypto/goolm/message/prekey_message.go | 120 +++ .../crypto/goolm/message/session_export.go | 44 + .../crypto/goolm/message/session_sharing.go | 50 ++ .../go/mautrix/crypto/goolm/olm/chain.go | 258 ++++++ .../go/mautrix/crypto/goolm/olm/olm.go | 432 +++++++++ .../crypto/goolm/olm/skipped_message.go | 55 ++ .../go/mautrix/crypto/goolm/pk/decryption.go | 165 ++++ .../go/mautrix/crypto/goolm/pk/encryption.go | 49 ++ .../go/mautrix/crypto/goolm/pk/signing.go | 44 + .../go/mautrix/crypto/goolm/sas/main.go | 76 ++ .../go/mautrix/crypto/goolm/session/main.go | 2 + .../goolm/session/megolm_inbound_session.go | 276 ++++++ .../goolm/session/megolm_outbound_session.go | 171 ++++ .../crypto/goolm/session/olm_session.go | 476 ++++++++++ .../go/mautrix/crypto/goolm/utilities/main.go | 23 + .../mautrix/crypto/goolm/utilities/pickle.go | 60 ++ .../go/mautrix/crypto/keyimport.go | 16 +- .../go/mautrix/crypto/keysharing.go | 39 +- .../maunium.net/go/mautrix/crypto/machine.go | 131 +-- .../maunium.net/go/mautrix/crypto/olm/LICENSE | 177 ---- .../go/mautrix/crypto/olm/README.md | 2 + .../go/mautrix/crypto/olm/account.go | 6 + .../go/mautrix/crypto/olm/account_goolm.go | 154 ++++ .../go/mautrix/crypto/olm/error.go | 2 + .../go/mautrix/crypto/olm/error_goolm.go | 23 + .../mautrix/crypto/olm/inboundgroupsession.go | 6 + .../crypto/olm/inboundgroupsession_goolm.go | 149 ++++ .../maunium.net/go/mautrix/crypto/olm/olm.go | 2 + .../go/mautrix/crypto/olm/olm_goolm.go | 20 + .../crypto/olm/outboundgroupsession.go | 6 + .../crypto/olm/outboundgroupsession_goolm.go | 111 +++ .../maunium.net/go/mautrix/crypto/olm/pk.go | 2 + .../go/mautrix/crypto/olm/pk_goolm.go | 71 ++ .../go/mautrix/crypto/olm/session.go | 6 + .../go/mautrix/crypto/olm/session_goolm.go | 110 +++ .../go/mautrix/crypto/olm/utility.go | 2 + .../go/mautrix/crypto/olm/utility_goolm.go | 92 ++ .../go/mautrix/crypto/olm/verification.go | 2 + .../mautrix/crypto/olm/verification_goolm.go | 23 + .../go/mautrix/crypto/sql_store.go | 527 ++++++----- .../sql_store_upgrade/00-latest-revision.sql | 5 +- .../sql_store_upgrade/11-outdated-devices.sql | 2 + .../crypto/sql_store_upgrade/upgrade.go | 3 +- .../go/mautrix/crypto/ssss/client.go | 35 +- vendor/maunium.net/go/mautrix/crypto/store.go | 168 ++-- .../go/mautrix/crypto/utils/utils.go | 2 +- .../go/mautrix/crypto/verification.go | 166 ++-- .../go/mautrix/crypto/verification_in_room.go | 77 +- vendor/maunium.net/go/mautrix/event/events.go | 2 + .../go/mautrix/event/eventsource.go | 72 ++ .../maunium.net/go/mautrix/event/message.go | 20 +- vendor/maunium.net/go/mautrix/requests.go | 4 +- .../go/mautrix/sqlstatestore/statestore.go | 309 +++---- .../v05-mark-encryption-state-resync.go | 7 +- vendor/maunium.net/go/mautrix/statestore.go | 136 +-- vendor/maunium.net/go/mautrix/sync.go | 145 +--- vendor/maunium.net/go/mautrix/syncstore.go | 53 +- vendor/maunium.net/go/mautrix/version.go | 2 +- vendor/maunium.net/go/mautrix/versions.go | 2 + vendor/modules.txt | 34 +- 237 files changed, 9091 insertions(+), 3317 deletions(-) create mode 100644 utils/psd.go create mode 100644 vendor/github.com/rs/zerolog/example.jsonl create mode 100644 vendor/go.mau.fi/util/dbutil/iter.go create mode 100644 vendor/go.mau.fi/util/dbutil/queryhelper.go create mode 100644 vendor/go.mau.fi/util/exerrors/must.go delete mode 100644 vendor/golang.org/x/crypto/blake2b/blake2b_amd64.go delete mode 100644 vendor/golang.org/x/crypto/internal/poly1305/bits_compat.go delete mode 100644 vendor/golang.org/x/crypto/internal/poly1305/bits_go1.13.go delete mode 100644 vendor/maunium.net/go/mautrix/crypto/canonicaljson/LICENSE create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/README.md create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/account/account.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/base64.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/cipher/aes_sha256.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/cipher/main.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/cipher/pickle.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/crypto/aes_cbc.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/crypto/main.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/errors.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/main.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/megolm/megolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/message/decoder.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/message/group_message.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/message/message.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/message/prekey_message.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/message/session_export.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/message/session_sharing.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/olm/chain.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/olm/olm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/olm/skipped_message.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/pk/decryption.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/pk/encryption.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/pk/signing.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/sas/main.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/session/main.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/session/megolm_inbound_session.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/session/megolm_outbound_session.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/session/olm_session.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/utilities/main.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/goolm/utilities/pickle.go delete mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/LICENSE create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/account_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/error_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/olm_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/pk_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/session_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/utility_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/olm/verification_goolm.go create mode 100644 vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/11-outdated-devices.sql create mode 100644 vendor/maunium.net/go/mautrix/event/eventsource.go diff --git a/bot/access.go b/bot/access.go index eddf8c9..14f2d5b 100644 --- a/bot/access.go +++ b/bot/access.go @@ -22,19 +22,19 @@ func parseMXIDpatterns(patterns []string, defaultPattern string) ([]*regexp.Rege return mxidwc.ParsePatterns(patterns) } -func (b *Bot) allowUsers(actorID id.UserID, targetRoomID id.RoomID) bool { +func (b *Bot) allowUsers(ctx context.Context, actorID id.UserID, targetRoomID id.RoomID) bool { // first, check if it's an allowed user if mxidwc.Match(actorID.String(), b.allowedUsers) { return true } // second, check if it's an admin (admin may not fit the allowed users pattern) - if b.allowAdmin(actorID, targetRoomID) { + if b.allowAdmin(ctx, actorID, targetRoomID) { return true } // then, check if it's the owner (same as above) - cfg, err := b.cfg.GetRoom(targetRoomID) + cfg, err := b.cfg.GetRoom(ctx, targetRoomID) if err == nil && cfg.Owner() == actorID.String() { return true } @@ -42,15 +42,15 @@ func (b *Bot) allowUsers(actorID id.UserID, targetRoomID id.RoomID) bool { return false } -func (b *Bot) allowAnyone(_ id.UserID, _ id.RoomID) bool { +func (b *Bot) allowAnyone(_ context.Context, _ id.UserID, _ id.RoomID) bool { return true } -func (b *Bot) allowOwner(actorID id.UserID, targetRoomID id.RoomID) bool { - if !b.allowUsers(actorID, targetRoomID) { +func (b *Bot) allowOwner(ctx context.Context, actorID id.UserID, targetRoomID id.RoomID) bool { + if !b.allowUsers(ctx, actorID, targetRoomID) { return false } - cfg, err := b.cfg.GetRoom(targetRoomID) + cfg, err := b.cfg.GetRoom(ctx, targetRoomID) if err != nil { b.Error(context.Background(), "failed to retrieve settings: %v", err) return false @@ -61,19 +61,19 @@ func (b *Bot) allowOwner(actorID id.UserID, targetRoomID id.RoomID) bool { return true } - return owner == actorID.String() || b.allowAdmin(actorID, targetRoomID) + return owner == actorID.String() || b.allowAdmin(ctx, actorID, targetRoomID) } -func (b *Bot) allowAdmin(actorID id.UserID, _ id.RoomID) bool { +func (b *Bot) allowAdmin(_ context.Context, actorID id.UserID, _ id.RoomID) bool { return mxidwc.Match(actorID.String(), b.allowedAdmins) } -func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool { - if !b.allowUsers(actorID, targetRoomID) { +func (b *Bot) allowSend(ctx context.Context, actorID id.UserID, targetRoomID id.RoomID) bool { + if !b.allowUsers(ctx, actorID, targetRoomID) { return false } - cfg, err := b.cfg.GetRoom(targetRoomID) + cfg, err := b.cfg.GetRoom(ctx, targetRoomID) if err != nil { b.Error(context.Background(), "failed to retrieve settings: %v", err) return false @@ -82,14 +82,14 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool { return !cfg.NoSend() } -func (b *Bot) allowReply(actorID id.UserID, targetRoomID id.RoomID) bool { - if !b.allowUsers(actorID, targetRoomID) { +func (b *Bot) allowReply(ctx context.Context, actorID id.UserID, targetRoomID id.RoomID) bool { + if !b.allowUsers(ctx, actorID, targetRoomID) { return false } - cfg, err := b.cfg.GetRoom(targetRoomID) + cfg, err := b.cfg.GetRoom(ctx, targetRoomID) if err != nil { - b.Error(context.Background(), "failed to retrieve settings: %v", err) + b.Error(ctx, "failed to retrieve settings: %v", err) return false } @@ -106,30 +106,30 @@ func (b *Bot) isReserved(mailbox string) bool { } // IsGreylisted checks if host is in greylist -func (b *Bot) IsGreylisted(addr net.Addr) bool { - if b.cfg.GetBot().Greylist() == 0 { +func (b *Bot) IsGreylisted(ctx context.Context, addr net.Addr) bool { + if b.cfg.GetBot(ctx).Greylist() == 0 { return false } - greylist := b.cfg.GetGreylist() + greylist := b.cfg.GetGreylist(ctx) greylistedAt, ok := greylist.Get(addr) if !ok { b.log.Debug().Str("addr", addr.String()).Msg("greylisting") greylist.Add(addr) - err := b.cfg.SetGreylist(greylist) + err := b.cfg.SetGreylist(ctx, greylist) if err != nil { b.log.Error().Err(err).Str("addr", addr.String()).Msg("cannot update greylist") } return true } - duration := time.Duration(b.cfg.GetBot().Greylist()) * time.Minute + duration := time.Duration(b.cfg.GetBot(ctx).Greylist()) * time.Minute return greylistedAt.Add(duration).After(time.Now().UTC()) } // IsBanned checks if address is banned -func (b *Bot) IsBanned(addr net.Addr) bool { - return b.cfg.GetBanlist().Has(addr) +func (b *Bot) IsBanned(ctx context.Context, addr net.Addr) bool { + return b.cfg.GetBanlist(ctx).Has(addr) } // IsTrusted checks if address is a trusted (proxy) @@ -146,12 +146,12 @@ func (b *Bot) IsTrusted(addr net.Addr) bool { } // Ban an address automatically -func (b *Bot) BanAuto(addr net.Addr) { - if !b.cfg.GetBot().BanlistEnabled() { +func (b *Bot) BanAuto(ctx context.Context, addr net.Addr) { + if !b.cfg.GetBot(ctx).BanlistEnabled() { return } - if !b.cfg.GetBot().BanlistAuto() { + if !b.cfg.GetBot(ctx).BanlistAuto() { return } @@ -159,21 +159,21 @@ func (b *Bot) BanAuto(addr net.Addr) { return } b.log.Debug().Str("addr", addr.String()).Msg("attempting to automatically ban") - banlist := b.cfg.GetBanlist() + banlist := b.cfg.GetBanlist(ctx) banlist.Add(addr) - err := b.cfg.SetBanlist(banlist) + err := b.cfg.SetBanlist(ctx, banlist) if err != nil { b.log.Error().Err(err).Str("addr", addr.String()).Msg("cannot update banlist") } } // Ban an address for incorrect auth automatically -func (b *Bot) BanAuth(addr net.Addr) { - if !b.cfg.GetBot().BanlistEnabled() { +func (b *Bot) BanAuth(ctx context.Context, addr net.Addr) { + if !b.cfg.GetBot(ctx).BanlistEnabled() { return } - if !b.cfg.GetBot().BanlistAuth() { + if !b.cfg.GetBot(ctx).BanlistAuth() { return } @@ -181,33 +181,33 @@ func (b *Bot) BanAuth(addr net.Addr) { return } b.log.Debug().Str("addr", addr.String()).Msg("attempting to automatically ban") - banlist := b.cfg.GetBanlist() + banlist := b.cfg.GetBanlist(ctx) banlist.Add(addr) - err := b.cfg.SetBanlist(banlist) + err := b.cfg.SetBanlist(ctx, banlist) if err != nil { b.log.Error().Err(err).Str("addr", addr.String()).Msg("cannot update banlist") } } // Ban an address manually -func (b *Bot) BanManually(addr net.Addr) { - if !b.cfg.GetBot().BanlistEnabled() { +func (b *Bot) BanManually(ctx context.Context, addr net.Addr) { + if !b.cfg.GetBot(ctx).BanlistEnabled() { return } if b.IsTrusted(addr) { return } b.log.Debug().Str("addr", addr.String()).Msg("attempting to manually ban") - banlist := b.cfg.GetBanlist() + banlist := b.cfg.GetBanlist(ctx) banlist.Add(addr) - err := b.cfg.SetBanlist(banlist) + err := b.cfg.SetBanlist(ctx, banlist) if err != nil { b.log.Error().Err(err).Str("addr", addr.String()).Msg("cannot update banlist") } } // AllowAuth check if SMTP login (email) and password are valid -func (b *Bot) AllowAuth(email, password string) (id.RoomID, bool) { +func (b *Bot) AllowAuth(ctx context.Context, email, password string) (id.RoomID, bool) { var suffix bool for _, domain := range b.domains { if strings.HasSuffix(email, "@"+domain) { @@ -223,7 +223,7 @@ func (b *Bot) AllowAuth(email, password string) (id.RoomID, bool) { if !ok { return "", false } - cfg, err := b.cfg.GetRoom(roomID) + cfg, err := b.cfg.GetRoom(ctx, roomID) if err != nil { b.log.Error().Err(err).Msg("failed to retrieve settings") return "", false diff --git a/bot/activation.go b/bot/activation.go index 40e3ed5..3d1f4dd 100644 --- a/bot/activation.go +++ b/bot/activation.go @@ -1,13 +1,14 @@ package bot import ( + "context" "fmt" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" ) -type activationFlow func(id.UserID, id.RoomID, string) bool +type activationFlow func(context.Context, id.UserID, id.RoomID, string) bool func (b *Bot) getActivationFlow() activationFlow { switch b.mbxc.Activation { @@ -21,19 +22,19 @@ func (b *Bot) getActivationFlow() activationFlow { } // ActivateMailbox using the configured flow -func (b *Bot) ActivateMailbox(ownerID id.UserID, roomID id.RoomID, mailbox string) bool { +func (b *Bot) ActivateMailbox(ctx context.Context, ownerID id.UserID, roomID id.RoomID, mailbox string) bool { flow := b.getActivationFlow() - return flow(ownerID, roomID, mailbox) + return flow(ctx, ownerID, roomID, mailbox) } -func (b *Bot) activateNone(ownerID id.UserID, roomID id.RoomID, mailbox string) bool { +func (b *Bot) activateNone(_ context.Context, ownerID id.UserID, roomID id.RoomID, mailbox string) bool { b.log.Debug().Str("mailbox", mailbox).Str("roomID", roomID.String()).Str("ownerID", ownerID.String()).Msg("activating mailbox through the flow 'none'") b.rooms.Store(mailbox, roomID) return true } -func (b *Bot) activateNotify(ownerID id.UserID, roomID id.RoomID, mailbox string) bool { +func (b *Bot) activateNotify(ctx context.Context, ownerID id.UserID, roomID id.RoomID, mailbox string) bool { b.log.Debug().Str("mailbox", mailbox).Str("roomID", roomID.String()).Str("ownerID", ownerID.String()).Msg("activating mailbox through the flow 'notify'") b.rooms.Store(mailbox, roomID) if len(b.adminRooms) == 0 { @@ -43,7 +44,7 @@ func (b *Bot) activateNotify(ownerID id.UserID, roomID id.RoomID, mailbox string msg := fmt.Sprintf("Mailbox %q has been registered by %q for the room %q", mailbox, ownerID, roomID) for _, adminRoom := range b.adminRooms { content := format.RenderMarkdown(msg, true, true) - _, err := b.lp.Send(adminRoom, &content) + _, err := b.lp.Send(ctx, adminRoom, &content) if err != nil { b.log.Info().Str("adminRoom", adminRoom.String()).Msg("cannot send mailbox activation notification to the admin room") continue diff --git a/bot/bot.go b/bot/bot.go index 77b6a54..0a9e02a 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -36,6 +36,7 @@ type Bot struct { rooms sync.Map proxies []string sendmail func(string, string, string) error + psd *utils.PSD cfg *config.Manager log *zerolog.Logger lp *linkpearl.Linkpearl @@ -50,6 +51,7 @@ func New( lp *linkpearl.Linkpearl, log *zerolog.Logger, cfg *config.Manager, + psd *utils.PSD, proxies []string, prefix string, domains []string, @@ -63,13 +65,14 @@ func New( adminRooms: []id.RoomID{}, proxies: proxies, mbxc: mbxc, + psd: psd, cfg: cfg, log: log, lp: lp, mu: utils.NewMutex(), q: q, } - users, err := b.initBotUsers() + users, err := b.initBotUsers(context.Background()) if err != nil { return nil, err } @@ -105,7 +108,7 @@ func (b *Bot) Error(ctx context.Context, message string, args ...any) { } var noThreads bool - cfg, cerr := b.cfg.GetRoom(evt.RoomID) + cfg, cerr := b.cfg.GetRoom(ctx, evt.RoomID) if cerr == nil { noThreads = cfg.NoThreads() } @@ -115,16 +118,17 @@ func (b *Bot) Error(ctx context.Context, message string, args ...any) { relatesTo = linkpearl.RelatesTo(threadID, noThreads) } - b.lp.SendNotice(evt.RoomID, "ERROR: "+err.Error(), relatesTo) + b.lp.SendNotice(ctx, evt.RoomID, "ERROR: "+err.Error(), relatesTo) } // Start performs matrix /sync func (b *Bot) Start(statusMsg string) error { - if err := b.migrateMautrix015(); err != nil { + ctx := context.Background() + if err := b.migrateMautrix015(ctx); err != nil { return err } - if err := b.syncRooms(); err != nil { + if err := b.syncRooms(ctx); err != nil { return err } @@ -135,7 +139,8 @@ func (b *Bot) Start(statusMsg string) error { // Stop the bot func (b *Bot) Stop() { - err := b.lp.GetClient().SetPresence(event.PresenceOffline) + ctx := context.Background() + err := b.lp.GetClient().SetPresence(ctx, event.PresenceOffline) if err != nil { b.log.Error().Err(err).Msg("cannot set presence = offline") } diff --git a/bot/command.go b/bot/command.go index c9379fe..fe99354 100644 --- a/bot/command.go +++ b/bot/command.go @@ -46,7 +46,7 @@ type ( key string description string sanitizer func(string) string - allowed func(id.UserID, id.RoomID) bool + allowed func(context.Context, id.UserID, id.RoomID) bool } commandList []command ) @@ -351,7 +351,7 @@ func (b *Bot) initCommands() commandList { func (b *Bot) handle(ctx context.Context) { evt := eventFromContext(ctx) - err := b.lp.GetClient().MarkRead(evt.RoomID, evt.ID) + err := b.lp.GetClient().MarkRead(ctx, evt.RoomID, evt.ID) if err != nil { b.log.Error().Err(err).Msg("cannot send read receipt") } @@ -378,14 +378,14 @@ func (b *Bot) handle(ctx context.Context) { if cmd == nil { return } - _, err = b.lp.GetClient().UserTyping(evt.RoomID, true, 30*time.Second) + _, err = b.lp.GetClient().UserTyping(ctx, evt.RoomID, true, 30*time.Second) if err != nil { b.log.Error().Err(err).Msg("cannot send typing notification") } - defer b.lp.GetClient().UserTyping(evt.RoomID, false, 30*time.Second) //nolint:errcheck // we don't care + defer b.lp.GetClient().UserTyping(ctx, evt.RoomID, false, 30*time.Second) //nolint:errcheck // we don't care - if !cmd.allowed(evt.Sender, evt.RoomID) { - b.lp.SendNotice(evt.RoomID, "not allowed to do that, kupo") + if !cmd.allowed(ctx, evt.Sender, evt.RoomID) { + b.lp.SendNotice(ctx, evt.RoomID, "not allowed to do that, kupo") return } @@ -452,7 +452,7 @@ func (b *Bot) parseCommand(message string, toLower bool) []string { return strings.Split(strings.TrimSpace(message), " ") } -func (b *Bot) sendIntroduction(roomID id.RoomID) { +func (b *Bot) sendIntroduction(ctx context.Context, roomID id.RoomID) { var msg strings.Builder msg.WriteString("Hello, kupo!\n\n") @@ -468,7 +468,7 @@ func (b *Bot) sendIntroduction(roomID id.RoomID) { msg.WriteString(utils.EmailsList("SOME_INBOX", "")) msg.WriteString("` and have them appear in this room.") - b.lp.SendNotice(roomID, msg.String()) + b.lp.SendNotice(ctx, roomID, msg.String()) } func (b *Bot) getHelpValue(cfg config.Room, cmd command) string { @@ -497,7 +497,7 @@ func (b *Bot) getHelpValue(cfg config.Room, cmd command) string { func (b *Bot) sendHelp(ctx context.Context) { evt := eventFromContext(ctx) - cfg, serr := b.cfg.GetRoom(evt.RoomID) + cfg, serr := b.cfg.GetRoom(ctx, evt.RoomID) if serr != nil { b.log.Error().Err(serr).Msg("cannot retrieve settings") } @@ -505,7 +505,7 @@ func (b *Bot) sendHelp(ctx context.Context) { var msg strings.Builder msg.WriteString("The following commands are supported and accessible to you:\n\n") for _, cmd := range b.commands { - if !cmd.allowed(evt.Sender, evt.RoomID) { + if !cmd.allowed(ctx, evt.Sender, evt.RoomID) { continue } if cmd.key == "" { @@ -528,7 +528,7 @@ func (b *Bot) sendHelp(ctx context.Context) { msg.WriteString("\n") } - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } func (b *Bot) runSend(ctx context.Context) { @@ -538,7 +538,7 @@ func (b *Bot) runSend(ctx context.Context) { return } - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "failed to retrieve room settings: %v", err) return @@ -555,11 +555,11 @@ func (b *Bot) runSend(ctx context.Context) { func (b *Bot) getSendDetails(ctx context.Context) (to, subject, body string, ok bool) { evt := eventFromContext(ctx) - if !b.allowSend(evt.Sender, evt.RoomID) { + if !b.allowSend(ctx, evt.Sender, evt.RoomID) { return "", "", "", false } - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "failed to retrieve room settings: %v", err) return "", "", "", false @@ -568,7 +568,7 @@ func (b *Bot) getSendDetails(ctx context.Context) (to, subject, body string, ok commandSlice := b.parseCommand(evt.Content.AsMessage().Body, false) to, subject, body, err = utils.ParseSend(commandSlice) if errors.Is(err, utils.ErrInvalidArgs) { - b.lp.SendNotice(evt.RoomID, fmt.Sprintf( + b.lp.SendNotice(ctx, evt.RoomID, fmt.Sprintf( "Usage:\n"+ "```\n"+ "%s send someone@example.com\n"+ @@ -585,7 +585,7 @@ func (b *Bot) getSendDetails(ctx context.Context) (to, subject, body string, ok mailbox := cfg.Mailbox() if mailbox == "" { - b.lp.SendNotice(evt.RoomID, "mailbox is not configured, kupo", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "mailbox is not configured, kupo", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) return "", "", "", false } @@ -608,8 +608,8 @@ func (b *Bot) runSendCommand(ctx context.Context, cfg config.Room, tos []string, } } - b.lock(evt.RoomID, evt.ID) - defer b.unlock(evt.RoomID, evt.ID) + b.lock(ctx, evt.RoomID, evt.ID) + defer b.unlock(ctx, evt.RoomID, evt.ID) domain := utils.SanitizeDomain(cfg.Domain()) from := cfg.Mailbox() + "@" + domain @@ -617,12 +617,12 @@ func (b *Bot) runSendCommand(ctx context.Context, cfg config.Room, tos []string, for _, to := range tos { recipients := []string{to} eml := email.New(ID, "", " "+ID, subject, from, to, to, "", body, htmlBody, nil, nil) - data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey()) + data := eml.Compose(b.cfg.GetBot(ctx).DKIMPrivateKey()) if data == "" { - b.lp.SendNotice(evt.RoomID, "email body is empty", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "email body is empty", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) return } - queued, err := b.Sendmail(evt.ID, from, to, data) + queued, err := b.Sendmail(ctx, evt.ID, from, to, data) if queued { b.log.Warn().Err(err).Msg("email has been queued") b.saveSentMetadata(ctx, queued, evt.ID, recipients, eml, cfg) @@ -635,6 +635,6 @@ func (b *Bot) runSendCommand(ctx context.Context, cfg config.Room, tos []string, b.saveSentMetadata(ctx, false, evt.ID, recipients, eml, cfg) } if len(tos) > 1 { - b.lp.SendNotice(evt.RoomID, "All emails were sent.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "All emails were sent.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } } diff --git a/bot/command_admin.go b/bot/command_admin.go index 70b3f79..0c21585 100644 --- a/bot/command_admin.go +++ b/bot/command_admin.go @@ -37,7 +37,7 @@ func (b *Bot) sendMailboxes(ctx context.Context) { if !ok { return true } - cfg, err := b.cfg.GetRoom(roomID) + cfg, err := b.cfg.GetRoom(ctx, roomID) if err != nil { b.log.Error().Err(err).Msg("cannot retrieve settings") } @@ -49,7 +49,7 @@ func (b *Bot) sendMailboxes(ctx context.Context) { sort.Strings(slice) if len(slice) == 0 { - b.lp.SendNotice(evt.RoomID, "No mailboxes are managed by the bot so far, kupo!", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "No mailboxes are managed by the bot so far, kupo!", linkpearl.RelatesTo(evt.ID)) return } @@ -64,20 +64,20 @@ func (b *Bot) sendMailboxes(ctx context.Context) { msg.WriteString("\n") } - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) } func (b *Bot) runDelete(ctx context.Context, commandSlice []string) { evt := eventFromContext(ctx) if len(commandSlice) < 2 { - b.lp.SendNotice(evt.RoomID, fmt.Sprintf("Usage: `%s delete MAILBOX`", b.prefix), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Usage: `%s delete MAILBOX`", b.prefix), linkpearl.RelatesTo(evt.ID)) return } mailbox := utils.Mailbox(commandSlice[1]) v, ok := b.rooms.Load(mailbox) if v == nil || !ok { - b.lp.SendNotice(evt.RoomID, "mailbox does not exists, kupo", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "mailbox does not exists, kupo", linkpearl.RelatesTo(evt.ID)) return } roomID, ok := v.(id.RoomID) @@ -86,18 +86,18 @@ func (b *Bot) runDelete(ctx context.Context, commandSlice []string) { } b.rooms.Delete(mailbox) - err := b.cfg.SetRoom(roomID, config.Room{}) + err := b.cfg.SetRoom(ctx, roomID, config.Room{}) if err != nil { b.Error(ctx, "cannot update settings: %v", err) return } - b.lp.SendNotice(evt.RoomID, "mailbox has been deleted", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "mailbox has been deleted", linkpearl.RelatesTo(evt.ID)) } func (b *Bot) runUsers(ctx context.Context, commandSlice []string) { evt := eventFromContext(ctx) - cfg := b.cfg.GetBot() + cfg := b.cfg.GetBot(ctx) if len(commandSlice) < 2 { var msg strings.Builder users := cfg.Users() @@ -112,35 +112,35 @@ func (b *Bot) runUsers(ctx context.Context, commandSlice []string) { msg.WriteString("where each pattern is like `@someone:example.com`, ") msg.WriteString("`@bot.*:example.com`, `@*:another.com`, or `@*:*`\n") - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) return } _, homeserver, err := b.lp.GetClient().UserID.Parse() if err != nil { - b.lp.SendNotice(evt.RoomID, fmt.Sprintf("invalid userID: %v", err), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, fmt.Sprintf("invalid userID: %v", err), linkpearl.RelatesTo(evt.ID)) } patterns := commandSlice[1:] allowedUsers, err := parseMXIDpatterns(patterns, "@*:"+homeserver) if err != nil { - b.lp.SendNotice(evt.RoomID, fmt.Sprintf("invalid patterns: %v", err), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, fmt.Sprintf("invalid patterns: %v", err), linkpearl.RelatesTo(evt.ID)) return } cfg.Set(config.BotUsers, strings.Join(patterns, " ")) - err = b.cfg.SetBot(cfg) + err = b.cfg.SetBot(ctx, cfg) if err != nil { b.Error(ctx, "cannot set bot config: %v", err) } b.allowedUsers = allowedUsers - b.lp.SendNotice(evt.RoomID, "allowed users updated", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "allowed users updated", linkpearl.RelatesTo(evt.ID)) } func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) { evt := eventFromContext(ctx) - cfg := b.cfg.GetBot() + cfg := b.cfg.GetBot(ctx) if len(commandSlice) > 1 && commandSlice[1] == "reset" { cfg.Set(config.BotDKIMPrivateKey, "") cfg.Set(config.BotDKIMSignature, "") @@ -157,14 +157,14 @@ func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) { } cfg.Set(config.BotDKIMSignature, signature) cfg.Set(config.BotDKIMPrivateKey, private) - err := b.cfg.SetBot(cfg) + err := b.cfg.SetBot(ctx, cfg) if err != nil { b.Error(ctx, "cannot save bot options: %v", err) return } } - b.lp.SendNotice(evt.RoomID, fmt.Sprintf( + b.lp.SendNotice(ctx, evt.RoomID, fmt.Sprintf( "DKIM signature is: `%s`.\n"+ "You need to add it to DNS records of all domains added to postmoogle (if not already):\n"+ "Add new DNS record with type = `TXT`, key (subdomain/from): `postmoogle._domainkey` and value (to):\n ```\n%s\n```\n"+ @@ -177,7 +177,7 @@ func (b *Bot) runDKIM(ctx context.Context, commandSlice []string) { func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) { evt := eventFromContext(ctx) - cfg := b.cfg.GetBot() + cfg := b.cfg.GetBot(ctx) if len(commandSlice) < 2 { var msg strings.Builder msg.WriteString("Currently: `") @@ -195,30 +195,30 @@ func (b *Bot) runCatchAll(ctx context.Context, commandSlice []string) { msg.WriteString(" catch-all MAILBOX`") msg.WriteString("where mailbox is valid and existing mailbox name\n") - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) return } mailbox := utils.Mailbox(commandSlice[1]) - _, ok := b.GetMapping(mailbox) + _, ok := b.GetMapping(ctx, mailbox) if !ok { - b.lp.SendNotice(evt.RoomID, "mailbox does not exist, kupo.", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "mailbox does not exist, kupo.", linkpearl.RelatesTo(evt.ID)) return } cfg.Set(config.BotCatchAll, mailbox) - err := b.cfg.SetBot(cfg) + err := b.cfg.SetBot(ctx, cfg) if err != nil { b.Error(ctx, "cannot save bot options: %v", err) return } - b.lp.SendNotice(evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, "")), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Catch-all is set to: `%s` (%s).", mailbox, utils.EmailsList(mailbox, "")), linkpearl.RelatesTo(evt.ID)) } func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) { evt := eventFromContext(ctx) - cfg := b.cfg.GetBot() + cfg := b.cfg.GetBot(ctx) if len(commandSlice) < 2 { var msg strings.Builder msg.WriteString("Currently: `") @@ -233,13 +233,13 @@ func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) { msg.WriteString(" adminroom ROOM_ID`") msg.WriteString("where ROOM_ID is valid and existing matrix room id\n") - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) return } roomID := b.parseCommand(evt.Content.AsMessage().Body, false)[1] // get original value, without forced lower case cfg.Set(config.BotAdminRoom, roomID) - err := b.cfg.SetBot(cfg) + err := b.cfg.SetBot(ctx, cfg) if err != nil { b.Error(ctx, "cannot save bot options: %v", err) return @@ -247,12 +247,12 @@ func (b *Bot) runAdminRoom(ctx context.Context, commandSlice []string) { b.adminRooms = append([]id.RoomID{id.RoomID(roomID)}, b.adminRooms...) // make it the first room in list on the fly - b.lp.SendNotice(evt.RoomID, fmt.Sprintf("Admin Room is set to: `%s`.", roomID), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Admin Room is set to: `%s`.", roomID), linkpearl.RelatesTo(evt.ID)) } func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) { - cfg := b.cfg.GetBot() - greylist := b.cfg.GetGreylist() + cfg := b.cfg.GetBot(ctx) + greylist := b.cfg.GetGreylist(ctx) var msg strings.Builder size := len(greylist) duration := cfg.Greylist() @@ -278,7 +278,7 @@ func (b *Bot) printGreylist(ctx context.Context, roomID id.RoomID) { msg.WriteString("where `MIN` is duration in minutes for automatic greylisting\n") } - b.lp.SendNotice(roomID, msg.String(), linkpearl.RelatesTo(eventFromContext(ctx).ID)) + b.lp.SendNotice(ctx, roomID, msg.String(), linkpearl.RelatesTo(eventFromContext(ctx).ID)) } func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) { @@ -287,21 +287,21 @@ func (b *Bot) runGreylist(ctx context.Context, commandSlice []string) { b.printGreylist(ctx, evt.RoomID) return } - cfg := b.cfg.GetBot() + cfg := b.cfg.GetBot(ctx) value := utils.SanitizeIntString(commandSlice[1]) cfg.Set(config.BotGreylist, value) - err := b.cfg.SetBot(cfg) + err := b.cfg.SetBot(ctx, cfg) if err != nil { b.Error(ctx, "cannot set bot config: %v", err) } - b.lp.SendNotice(evt.RoomID, "greylist duration has been updated", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "greylist duration has been updated", linkpearl.RelatesTo(evt.ID)) } func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) { evt := eventFromContext(ctx) - cfg := b.cfg.GetBot() + cfg := b.cfg.GetBot(ctx) if len(commandSlice) < 2 { - banlist := b.cfg.GetBanlist() + banlist := b.cfg.GetBanlist(ctx) var msg strings.Builder size := len(banlist) if size > 0 { @@ -322,26 +322,26 @@ func (b *Bot) runBanlist(ctx context.Context, commandSlice []string) { msg.WriteString("where each ip is IPv4 or IPv6\n\n") msg.WriteString("You can find current banlist values below:\n") - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) b.addBanlistTimeline(ctx, false) return } value := utils.SanitizeBoolString(commandSlice[1]) cfg.Set(config.BotBanlistEnabled, value) - err := b.cfg.SetBot(cfg) + err := b.cfg.SetBot(ctx, cfg) if err != nil { b.Error(ctx, "cannot set bot config: %v", err) } - b.lp.SendNotice(evt.RoomID, "banlist has been updated", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "banlist has been updated", linkpearl.RelatesTo(evt.ID)) } func (b *Bot) runBanlistTotals(ctx context.Context) { evt := eventFromContext(ctx) - banlist := b.cfg.GetBanlist() + banlist := b.cfg.GetBanlist(ctx) var msg strings.Builder size := len(banlist) if size == 0 { - b.lp.SendNotice(evt.RoomID, "banlist is empty, kupo.", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "banlist is empty, kupo.", linkpearl.RelatesTo(evt.ID)) return } @@ -349,13 +349,13 @@ func (b *Bot) runBanlistTotals(ctx context.Context) { msg.WriteString(strconv.Itoa(size)) msg.WriteString(" hosts banned\n\n") msg.WriteString("You can find daily totals below:\n") - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) b.addBanlistTimeline(ctx, true) } func (b *Bot) runBanlistAuth(ctx context.Context, commandSlice []string) { //nolint:dupl // not in that case evt := eventFromContext(ctx) - cfg := b.cfg.GetBot() + cfg := b.cfg.GetBot(ctx) if len(commandSlice) < 2 { var msg strings.Builder msg.WriteString("Currently: `") @@ -368,21 +368,21 @@ func (b *Bot) runBanlistAuth(ctx context.Context, commandSlice []string) { //nol msg.WriteString(" banlist:auth true` (banlist itself must be enabled!)\n\n") } - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) return } value := utils.SanitizeBoolString(commandSlice[1]) cfg.Set(config.BotBanlistAuth, value) - err := b.cfg.SetBot(cfg) + err := b.cfg.SetBot(ctx, cfg) if err != nil { b.Error(ctx, "cannot set bot config: %v", err) } - b.lp.SendNotice(evt.RoomID, "auth banning has been updated", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "auth banning has been updated", linkpearl.RelatesTo(evt.ID)) } func (b *Bot) runBanlistAuto(ctx context.Context, commandSlice []string) { //nolint:dupl // not in that case evt := eventFromContext(ctx) - cfg := b.cfg.GetBot() + cfg := b.cfg.GetBot(ctx) if len(commandSlice) < 2 { var msg strings.Builder msg.WriteString("Currently: `") @@ -395,16 +395,16 @@ func (b *Bot) runBanlistAuto(ctx context.Context, commandSlice []string) { //nol msg.WriteString(" banlist:auto true` (banlist itself must be enabled!)\n\n") } - b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID)) return } value := utils.SanitizeBoolString(commandSlice[1]) cfg.Set(config.BotBanlistAuto, value) - err := b.cfg.SetBot(cfg) + err := b.cfg.SetBot(ctx, cfg) if err != nil { b.Error(ctx, "cannot set bot config: %v", err) } - b.lp.SendNotice(evt.RoomID, "auto banning has been updated", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "auto banning has been updated", linkpearl.RelatesTo(evt.ID)) } func (b *Bot) runBanlistChange(ctx context.Context, mode string, commandSlice []string) { @@ -413,11 +413,11 @@ func (b *Bot) runBanlistChange(ctx context.Context, mode string, commandSlice [] b.runBanlist(ctx, commandSlice) return } - if !b.cfg.GetBot().BanlistEnabled() { - b.lp.SendNotice(evt.RoomID, "banlist is disabled, you have to enable it first, kupo", linkpearl.RelatesTo(evt.ID)) + if !b.cfg.GetBot(ctx).BanlistEnabled() { + b.lp.SendNotice(ctx, evt.RoomID, "banlist is disabled, you have to enable it first, kupo", linkpearl.RelatesTo(evt.ID)) return } - banlist := b.cfg.GetBanlist() + banlist := b.cfg.GetBanlist(ctx) var action func(net.Addr) if mode == "remove" { @@ -436,18 +436,18 @@ func (b *Bot) runBanlistChange(ctx context.Context, mode string, commandSlice [] action(addr) } - err := b.cfg.SetBanlist(banlist) + err := b.cfg.SetBanlist(ctx, banlist) if err != nil { b.Error(ctx, "cannot set banlist: %v", err) return } - b.lp.SendNotice(evt.RoomID, "banlist has been updated, kupo", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "banlist has been updated, kupo", linkpearl.RelatesTo(evt.ID)) } func (b *Bot) addBanlistTimeline(ctx context.Context, onlyTotals bool) { evt := eventFromContext(ctx) - banlist := b.cfg.GetBanlist() + banlist := b.cfg.GetBanlist(ctx) timeline := map[string][]string{} for ip, ts := range banlist { key := "???" @@ -479,22 +479,22 @@ func (b *Bot) addBanlistTimeline(ctx context.Context, onlyTotals bool) { txt.WriteString(strings.Join(data, "`, `")) txt.WriteString("`\n") } - b.lp.SendNotice(evt.RoomID, txt.String(), linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, txt.String(), linkpearl.RelatesTo(evt.ID)) } } func (b *Bot) runBanlistReset(ctx context.Context) { evt := eventFromContext(ctx) - if !b.cfg.GetBot().BanlistEnabled() { - b.lp.SendNotice(evt.RoomID, "banlist is disabled, you have to enable it first, kupo", linkpearl.RelatesTo(evt.ID)) + if !b.cfg.GetBot(ctx).BanlistEnabled() { + b.lp.SendNotice(ctx, evt.RoomID, "banlist is disabled, you have to enable it first, kupo", linkpearl.RelatesTo(evt.ID)) return } - err := b.cfg.SetBanlist(config.List{}) + err := b.cfg.SetBanlist(ctx, config.List{}) if err != nil { b.Error(ctx, "cannot set banlist: %v", err) return } - b.lp.SendNotice(evt.RoomID, "banlist has been reset, kupo", linkpearl.RelatesTo(evt.ID)) + b.lp.SendNotice(ctx, evt.RoomID, "banlist has been reset, kupo", linkpearl.RelatesTo(evt.ID)) } diff --git a/bot/command_owner.go b/bot/command_owner.go index 1120931..a175d58 100644 --- a/bot/command_owner.go +++ b/bot/command_owner.go @@ -16,7 +16,7 @@ import ( func (b *Bot) runStop(ctx context.Context) { evt := eventFromContext(ctx) - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "failed to retrieve settings: %v", err) return @@ -24,19 +24,19 @@ func (b *Bot) runStop(ctx context.Context) { mailbox := cfg.Get(config.RoomMailbox) if mailbox == "" { - b.lp.SendNotice(evt.RoomID, "that room is not configured yet", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "that room is not configured yet", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) return } b.rooms.Delete(mailbox) - err = b.cfg.SetRoom(evt.RoomID, config.Room{}) + err = b.cfg.SetRoom(ctx, evt.RoomID, config.Room{}) if err != nil { b.Error(ctx, "cannot update settings: %v", err) return } - b.lp.SendNotice(evt.RoomID, "mailbox has been disabled", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "mailbox has been disabled", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } func (b *Bot) handleOption(ctx context.Context, cmd []string) { @@ -58,7 +58,7 @@ func (b *Bot) handleOption(ctx context.Context, cmd []string) { func (b *Bot) getOption(ctx context.Context, name string) { evt := eventFromContext(ctx) - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "failed to retrieve settings: %v", err) return @@ -73,7 +73,7 @@ func (b *Bot) getOption(ctx context.Context, name string) { msg := fmt.Sprintf("`%s` is not set, kupo.\n"+ "To set it, send a `%s %s VALUE` command.", name, b.prefix, name) - b.lp.SendNotice(evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) return } @@ -91,18 +91,18 @@ func (b *Bot) getOption(ctx context.Context, name string) { "or just set a new one with `%s %s NEW_PASSWORD`.", b.prefix, name) } - b.lp.SendNotice(evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } func (b *Bot) setMailbox(ctx context.Context, value string) { evt := eventFromContext(ctx) existingID, ok := b.getMapping(value) if (ok && existingID != "" && existingID != evt.RoomID) || b.isReserved(value) { - b.lp.SendNotice(evt.RoomID, fmt.Sprintf("Mailbox `%s` (%s) already taken, kupo", value, utils.EmailsList(value, ""))) + b.lp.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Mailbox `%s` (%s) already taken, kupo", value, utils.EmailsList(value, ""))) return } - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "failed to retrieve settings: %v", err) return @@ -113,23 +113,23 @@ func (b *Bot) setMailbox(ctx context.Context, value string) { if old != "" { b.rooms.Delete(old) } - active := b.ActivateMailbox(evt.Sender, evt.RoomID, value) + active := b.ActivateMailbox(ctx, evt.Sender, evt.RoomID, value) cfg.Set(config.RoomActive, strconv.FormatBool(active)) value = fmt.Sprintf("%s@%s", value, utils.SanitizeDomain(cfg.Domain())) - err = b.cfg.SetRoom(evt.RoomID, cfg) + err = b.cfg.SetRoom(ctx, evt.RoomID, cfg) if err != nil { b.Error(ctx, "cannot update settings: %v", err) return } msg := fmt.Sprintf("mailbox of this room set to `%s`", value) - b.lp.SendNotice(evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } func (b *Bot) setPassword(ctx context.Context) { evt := eventFromContext(ctx) - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "failed to retrieve settings: %v", err) return @@ -143,13 +143,13 @@ func (b *Bot) setPassword(ctx context.Context) { } cfg.Set(config.RoomPassword, value) - err = b.cfg.SetRoom(evt.RoomID, cfg) + err = b.cfg.SetRoom(ctx, evt.RoomID, cfg) if err != nil { b.Error(ctx, "cannot update settings: %v", err) return } - b.lp.SendNotice(evt.RoomID, "SMTP password has been set", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "SMTP password has been set", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } func (b *Bot) setOption(ctx context.Context, name, value string) { @@ -159,7 +159,7 @@ func (b *Bot) setOption(ctx context.Context, name, value string) { } evt := eventFromContext(ctx) - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "failed to retrieve settings: %v", err) return @@ -176,19 +176,19 @@ func (b *Bot) setOption(ctx context.Context, name, value string) { old := cfg.Get(name) if old == value { - b.lp.SendNotice(evt.RoomID, "nothing changed, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "nothing changed, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) return } cfg.Set(name, value) - err = b.cfg.SetRoom(evt.RoomID, cfg) + err = b.cfg.SetRoom(ctx, evt.RoomID, cfg) if err != nil { b.Error(ctx, "cannot update settings: %v", err) return } msg := fmt.Sprintf("`%s` of this room set to:\n```\n%s\n```", name, value) - b.lp.SendNotice(evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, msg, linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } func (b *Bot) runSpamlistAdd(ctx context.Context, commandSlice []string) { @@ -197,7 +197,7 @@ func (b *Bot) runSpamlistAdd(ctx context.Context, commandSlice []string) { b.getOption(ctx, config.RoomSpamlist) return } - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "cannot get room settings: %v", err) return @@ -212,7 +212,7 @@ func (b *Bot) runSpamlistAdd(ctx context.Context, commandSlice []string) { } cfg.Set(config.RoomSpamlist, utils.SliceString(spamlist)) - err = b.cfg.SetRoom(evt.RoomID, cfg) + err = b.cfg.SetRoom(ctx, evt.RoomID, cfg) if err != nil { b.Error(ctx, "cannot store room settings: %v", err) return @@ -223,7 +223,7 @@ func (b *Bot) runSpamlistAdd(ctx context.Context, commandSlice []string) { threadID = evt.ID } - b.lp.SendNotice(evt.RoomID, "spamlist has been updated, kupo", linkpearl.RelatesTo(threadID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "spamlist has been updated, kupo", linkpearl.RelatesTo(threadID, cfg.NoThreads())) } func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) { @@ -232,7 +232,7 @@ func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) { b.getOption(ctx, config.RoomSpamlist) return } - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "cannot get room settings: %v", err) return @@ -248,7 +248,7 @@ func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) { toRemove[idx] = struct{}{} } if len(toRemove) == 0 { - b.lp.SendNotice(evt.RoomID, "nothing new, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "nothing new, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) return } @@ -261,34 +261,34 @@ func (b *Bot) runSpamlistRemove(ctx context.Context, commandSlice []string) { } cfg.Set(config.RoomSpamlist, utils.SliceString(updatedSpamlist)) - err = b.cfg.SetRoom(evt.RoomID, cfg) + err = b.cfg.SetRoom(ctx, evt.RoomID, cfg) if err != nil { b.Error(ctx, "cannot store room settings: %v", err) return } - b.lp.SendNotice(evt.RoomID, "spamlist has been updated, kupo", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "spamlist has been updated, kupo", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } func (b *Bot) runSpamlistReset(ctx context.Context) { evt := eventFromContext(ctx) - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "cannot get room settings: %v", err) return } spamlist := utils.StringSlice(cfg[config.RoomSpamlist]) if len(spamlist) == 0 { - b.lp.SendNotice(evt.RoomID, "spamlist is empty, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "spamlist is empty, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) return } cfg.Set(config.RoomSpamlist, "") - err = b.cfg.SetRoom(evt.RoomID, cfg) + err = b.cfg.SetRoom(ctx, evt.RoomID, cfg) if err != nil { b.Error(ctx, "cannot store room settings: %v", err) return } - b.lp.SendNotice(evt.RoomID, "spamlist has been reset, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "spamlist has been reset, kupo.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads())) } diff --git a/bot/config/manager.go b/bot/config/manager.go index 97d8a68..780cb0f 100644 --- a/bot/config/manager.go +++ b/bot/config/manager.go @@ -1,6 +1,8 @@ package config import ( + "context" + "github.com/rs/zerolog" "gitlab.com/etke.cc/linkpearl" "maunium.net/go/mautrix/id" @@ -27,10 +29,10 @@ func New(lp *linkpearl.Linkpearl, log *zerolog.Logger) *Manager { } // GetBot config -func (m *Manager) GetBot() Bot { +func (m *Manager) GetBot(ctx context.Context) Bot { var err error var config Bot - config, err = m.lp.GetAccountData(acBotKey) + config, err = m.lp.GetAccountData(ctx, acBotKey) if err != nil { m.log.Error().Err(err).Msg("cannot get bot settings") } @@ -43,13 +45,13 @@ func (m *Manager) GetBot() Bot { } // SetBot config -func (m *Manager) SetBot(cfg Bot) error { - return m.lp.SetAccountData(acBotKey, cfg) +func (m *Manager) SetBot(ctx context.Context, cfg Bot) error { + return m.lp.SetAccountData(ctx, acBotKey, cfg) } // GetRoom config -func (m *Manager) GetRoom(roomID id.RoomID) (Room, error) { - config, err := m.lp.GetRoomAccountData(roomID, acRoomKey) +func (m *Manager) GetRoom(ctx context.Context, roomID id.RoomID) (Room, error) { + config, err := m.lp.GetRoomAccountData(ctx, roomID, acRoomKey) if err != nil { m.log.Warn().Err(err).Str("room_id", roomID.String()).Msg("cannot get room settings") } @@ -61,19 +63,19 @@ func (m *Manager) GetRoom(roomID id.RoomID) (Room, error) { } // SetRoom config -func (m *Manager) SetRoom(roomID id.RoomID, cfg Room) error { - return m.lp.SetRoomAccountData(roomID, acRoomKey, cfg) +func (m *Manager) SetRoom(ctx context.Context, roomID id.RoomID, cfg Room) error { + return m.lp.SetRoomAccountData(ctx, roomID, acRoomKey, cfg) } // GetBanlist config -func (m *Manager) GetBanlist() List { - if !m.GetBot().BanlistEnabled() { +func (m *Manager) GetBanlist(ctx context.Context) List { + if !m.GetBot(ctx).BanlistEnabled() { return make(List, 0) } m.mu.Lock("banlist") defer m.mu.Unlock("banlist") - config, err := m.lp.GetAccountData(acBanlistKey) + config, err := m.lp.GetAccountData(ctx, acBanlistKey) if err != nil { m.log.Error().Err(err).Msg("cannot get banlist") } @@ -85,8 +87,8 @@ func (m *Manager) GetBanlist() List { } // SetBanlist config -func (m *Manager) SetBanlist(cfg List) error { - if !m.GetBot().BanlistEnabled() { +func (m *Manager) SetBanlist(ctx context.Context, cfg List) error { + if !m.GetBot(ctx).BanlistEnabled() { return nil } @@ -96,12 +98,12 @@ func (m *Manager) SetBanlist(cfg List) error { cfg = make(List, 0) } - return m.lp.SetAccountData(acBanlistKey, cfg) + return m.lp.SetAccountData(ctx, acBanlistKey, cfg) } // GetGreylist config -func (m *Manager) GetGreylist() List { - config, err := m.lp.GetAccountData(acGreylistKey) +func (m *Manager) GetGreylist(ctx context.Context) List { + config, err := m.lp.GetAccountData(ctx, acGreylistKey) if err != nil { m.log.Error().Err(err).Msg("cannot get banlist") } @@ -114,6 +116,6 @@ func (m *Manager) GetGreylist() List { } // SetGreylist config -func (m *Manager) SetGreylist(cfg List) error { - return m.lp.SetAccountData(acGreylistKey, cfg) +func (m *Manager) SetGreylist(ctx context.Context, cfg List) error { + return m.lp.SetAccountData(ctx, acGreylistKey, cfg) } diff --git a/bot/context.go b/bot/context.go index f4bf4e8..3043e2b 100644 --- a/bot/context.go +++ b/bot/context.go @@ -15,8 +15,10 @@ const ( ctxThreadID ctxkey = iota ) -func newContext(evt *event.Event) context.Context { - ctx := context.Background() +func newContext(ctx context.Context, evt *event.Event) context.Context { + if ctx == nil { + ctx = context.Background() + } hub := sentry.CurrentHub().Clone() ctx = sentry.SetHubOnContext(ctx, hub) ctx = eventToContext(ctx, evt) diff --git a/bot/data.go b/bot/data.go index 851397e..548411c 100644 --- a/bot/data.go +++ b/bot/data.go @@ -1,6 +1,7 @@ package bot import ( + "context" "strconv" "time" @@ -9,21 +10,21 @@ import ( "gitlab.com/etke.cc/postmoogle/bot/config" ) -func (b *Bot) syncRooms() error { +func (b *Bot) syncRooms(ctx context.Context) error { adminRooms := []id.RoomID{} - adminRoom := b.cfg.GetBot().AdminRoom() + adminRoom := b.cfg.GetBot(ctx).AdminRoom() if adminRoom != "" { adminRooms = append(adminRooms, adminRoom) } - resp, err := b.lp.GetClient().JoinedRooms() + resp, err := b.lp.GetClient().JoinedRooms(ctx) if err != nil { return err } for _, roomID := range resp.JoinedRooms { - b.migrateRoomSettings(roomID) - cfg, serr := b.cfg.GetRoom(roomID) + b.migrateRoomSettings(ctx, roomID) + cfg, serr := b.cfg.GetRoom(ctx, roomID) if serr != nil { continue } @@ -33,7 +34,7 @@ func (b *Bot) syncRooms() error { b.rooms.Store(mailbox, roomID) } - if cfg.Owner() != "" && b.allowAdmin(id.UserID(cfg.Owner()), "") { + if cfg.Owner() != "" && b.allowAdmin(ctx, id.UserID(cfg.Owner()), "") { adminRooms = append(adminRooms, roomID) } } @@ -42,8 +43,8 @@ func (b *Bot) syncRooms() error { return nil } -func (b *Bot) migrateRoomSettings(roomID id.RoomID) { - cfg, err := b.cfg.GetRoom(roomID) +func (b *Bot) migrateRoomSettings(ctx context.Context, roomID id.RoomID) { + cfg, err := b.cfg.GetRoom(ctx, roomID) if err != nil { b.log.Error().Err(err).Msg("cannot retrieve room settings") return @@ -56,7 +57,7 @@ func (b *Bot) migrateRoomSettings(roomID id.RoomID) { return } cfg.MigrateSpamlistSettings() - err = b.cfg.SetRoom(roomID, cfg) + err = b.cfg.SetRoom(ctx, roomID, cfg) if err != nil { b.log.Error().Err(err).Msg("cannot migrate room settings") } @@ -68,8 +69,8 @@ func (b *Bot) migrateRoomSettings(roomID id.RoomID) { // alongside with other database configs to simplify maintenance, // but with that simplification there is no proper way to migrate // existing sync token and session info. No data loss, tho. -func (b *Bot) migrateMautrix015() error { - cfg := b.cfg.GetBot() +func (b *Bot) migrateMautrix015(ctx context.Context) error { + cfg := b.cfg.GetBot(ctx) ts := cfg.Mautrix015Migration() // already migrated if ts > 0 { @@ -82,11 +83,11 @@ func (b *Bot) migrateMautrix015() error { tss := strconv.FormatInt(ts, 10) cfg.Set(config.BotMautrix015Migration, tss) - return b.cfg.SetBot(cfg) + return b.cfg.SetBot(ctx, cfg) } -func (b *Bot) initBotUsers() ([]string, error) { - cfg := b.cfg.GetBot() +func (b *Bot) initBotUsers(ctx context.Context) ([]string, error) { + cfg := b.cfg.GetBot(ctx) cfgUsers := cfg.Users() if len(cfgUsers) > 0 { return cfgUsers, nil @@ -97,10 +98,10 @@ func (b *Bot) initBotUsers() ([]string, error) { return nil, err } cfg.Set(config.BotUsers, "@*:"+homeserver) - return cfg.Users(), b.cfg.SetBot(cfg) + return cfg.Users(), b.cfg.SetBot(ctx, cfg) } // SyncRooms and mailboxes func (b *Bot) SyncRooms() { - b.syncRooms() //nolint:errcheck // nothing can be done here + b.syncRooms(context.Background()) //nolint:errcheck // nothing can be done here } diff --git a/bot/email.go b/bot/email.go index 0b1061d..3983b88 100644 --- a/bot/email.go +++ b/bot/email.go @@ -60,14 +60,14 @@ func (b *Bot) shouldQueue(msg string) bool { // Sendmail tries to send email immediately, but if it gets 4xx error (greylisting), // the email will be added to the queue and retried several times after that -func (b *Bot) Sendmail(eventID id.EventID, from, to, data string) (bool, error) { +func (b *Bot) Sendmail(ctx context.Context, eventID id.EventID, from, to, data string) (bool, error) { log := b.log.With().Str("from", from).Str("to", to).Str("eventID", eventID.String()).Logger() log.Info().Msg("attempting to deliver email") err := b.sendmail(from, to, data) if err != nil { if b.shouldQueue(err.Error()) { log.Info().Err(err).Msg("email has been added to the queue") - return true, b.q.Add(eventID.String(), from, to, data) + return true, b.q.Add(ctx, eventID.String(), from, to, data) } log.Warn().Err(err).Msg("email delivery failed") return false, err @@ -78,8 +78,8 @@ func (b *Bot) Sendmail(eventID id.EventID, from, to, data string) (bool, error) } // GetDKIMprivkey returns DKIM private key -func (b *Bot) GetDKIMprivkey() string { - return b.cfg.GetBot().DKIMPrivateKey() +func (b *Bot) GetDKIMprivkey(ctx context.Context) string { + return b.cfg.GetBot(ctx).DKIMPrivateKey() } func (b *Bot) getMapping(mailbox string) (id.RoomID, bool) { @@ -97,10 +97,10 @@ func (b *Bot) getMapping(mailbox string) (id.RoomID, bool) { } // GetMapping returns mapping of mailbox = room -func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) { +func (b *Bot) GetMapping(ctx context.Context, mailbox string) (id.RoomID, bool) { roomID, ok := b.getMapping(mailbox) if !ok { - catchAll := b.cfg.GetBot().CatchAll() + catchAll := b.cfg.GetBot(ctx).CatchAll() if catchAll == "" { return roomID, ok } @@ -111,8 +111,8 @@ func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) { } // GetIFOptions returns incoming email filtering options (room settings) -func (b *Bot) GetIFOptions(roomID id.RoomID) email.IncomingFilteringOptions { - cfg, err := b.cfg.GetRoom(roomID) +func (b *Bot) GetIFOptions(ctx context.Context, roomID id.RoomID) email.IncomingFilteringOptions { + cfg, err := b.cfg.GetRoom(ctx, roomID) if err != nil { b.log.Error().Err(err).Msg("cannot retrieve room settings") } @@ -124,11 +124,11 @@ func (b *Bot) GetIFOptions(roomID id.RoomID) email.IncomingFilteringOptions { // //nolint:gocognit // TODO func (b *Bot) IncomingEmail(ctx context.Context, eml *email.Email) error { - roomID, ok := b.GetMapping(eml.Mailbox(true)) + roomID, ok := b.GetMapping(ctx, eml.Mailbox(true)) if !ok { return ErrNoRoom } - cfg, err := b.cfg.GetRoom(roomID) + cfg, err := b.cfg.GetRoom(ctx, roomID) if err != nil { b.Error(ctx, "cannot get settings: %v", err) } @@ -139,15 +139,15 @@ func (b *Bot) IncomingEmail(ctx context.Context, eml *email.Email) error { var threadID id.EventID newThread := true if eml.InReplyTo != "" || eml.References != "" { - threadID = b.getThreadID(roomID, eml.InReplyTo, eml.References) + threadID = b.getThreadID(ctx, roomID, eml.InReplyTo, eml.References) if threadID != "" { newThread = false ctx = threadIDToContext(ctx, threadID) - b.setThreadID(roomID, eml.MessageID, threadID) + b.setThreadID(ctx, roomID, eml.MessageID, threadID) } } - content := eml.Content(threadID, cfg.ContentOptions()) - eventID, serr := b.lp.Send(roomID, content) + content := eml.Content(threadID, cfg.ContentOptions(), b.psd) + eventID, serr := b.lp.Send(ctx, roomID, content) if serr != nil { if !strings.Contains(serr.Error(), "M_UNKNOWN") { // if it's not an unknown event error return serr @@ -160,11 +160,11 @@ func (b *Bot) IncomingEmail(ctx context.Context, eml *email.Email) error { ctx = threadIDToContext(ctx, threadID) } - b.setThreadID(roomID, eml.MessageID, threadID) - b.setLastEventID(roomID, threadID, eventID) + b.setThreadID(ctx, roomID, eml.MessageID, threadID) + b.setLastEventID(ctx, roomID, threadID, eventID) if newThread && cfg.Threadify() { - _, berr := b.lp.Send(roomID, eml.ContentBody(threadID, cfg.ContentOptions())) + _, berr := b.lp.Send(ctx, roomID, eml.ContentBody(threadID, cfg.ContentOptions())) if berr != nil { return berr } @@ -179,15 +179,15 @@ func (b *Bot) IncomingEmail(ctx context.Context, eml *email.Email) error { } if newThread && cfg.Autoreply() != "" { - b.sendAutoreply(roomID, threadID) + b.sendAutoreply(ctx, roomID, threadID) } return nil } //nolint:gocognit // TODO -func (b *Bot) sendAutoreply(roomID id.RoomID, threadID id.EventID) { - cfg, err := b.cfg.GetRoom(roomID) +func (b *Bot) sendAutoreply(ctx context.Context, roomID id.RoomID, threadID id.EventID) { + cfg, err := b.cfg.GetRoom(ctx, roomID) if err != nil { return } @@ -197,7 +197,7 @@ func (b *Bot) sendAutoreply(roomID id.RoomID, threadID id.EventID) { return } - threadEvt, err := b.lp.GetClient().GetEvent(roomID, threadID) + threadEvt, err := b.lp.GetClient().GetEvent(ctx, roomID, threadID) if err != nil { b.log.Error().Err(err).Msg("cannot get thread event for autoreply") return @@ -216,7 +216,7 @@ func (b *Bot) sendAutoreply(roomID id.RoomID, threadID id.EventID) { }, } - meta := b.getParentEmail(evt, cfg.Mailbox()) + meta := b.getParentEmail(ctx, evt, cfg.Mailbox()) if meta.To == "" { return @@ -246,16 +246,16 @@ func (b *Bot) sendAutoreply(roomID id.RoomID, threadID id.EventID) { meta.References = meta.References + " " + meta.MessageID b.log.Info().Any("meta", meta).Msg("sending automatic reply") eml := email.New(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, meta.RcptTo, meta.CC, body, htmlBody, nil, nil) - data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey()) + data := eml.Compose(b.cfg.GetBot(ctx).DKIMPrivateKey()) if data == "" { return } var queued bool - ctx := newContext(threadEvt) + ctx = newContext(ctx, threadEvt) recipients := meta.Recipients for _, to := range recipients { - queued, err = b.Sendmail(evt.ID, meta.From, to, data) + queued, err = b.Sendmail(ctx, evt.ID, meta.From, to, data) if queued { b.log.Info().Err(err).Str("from", meta.From).Str("to", to).Msg("email has been queued") b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, cfg, "Autoreply has been sent to "+to+" (queued)") @@ -273,7 +273,7 @@ func (b *Bot) sendAutoreply(roomID id.RoomID, threadID id.EventID) { func (b *Bot) canReply(ctx context.Context) bool { evt := eventFromContext(ctx) - return b.allowSend(evt.Sender, evt.RoomID) && b.allowReply(evt.Sender, evt.RoomID) + return b.allowSend(ctx, evt.Sender, evt.RoomID) && b.allowReply(ctx, evt.Sender, evt.RoomID) } // SendEmailReply sends replies from matrix thread to email thread @@ -284,7 +284,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) { if !b.canReply(ctx) { return } - cfg, err := b.cfg.GetRoom(evt.RoomID) + cfg, err := b.cfg.GetRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "cannot retrieve room settings: %v", err) return @@ -295,10 +295,10 @@ func (b *Bot) SendEmailReply(ctx context.Context) { return } - b.lock(evt.RoomID, evt.ID) - defer b.unlock(evt.RoomID, evt.ID) + b.lock(ctx, evt.RoomID, evt.ID) + defer b.unlock(ctx, evt.RoomID, evt.ID) - meta := b.getParentEmail(evt, mailbox) + meta := b.getParentEmail(ctx, evt, mailbox) if meta.To == "" { b.Error(ctx, "cannot find parent email and continue the thread. Please, start a new email thread") @@ -306,7 +306,7 @@ func (b *Bot) SendEmailReply(ctx context.Context) { } if meta.ThreadID == "" { - meta.ThreadID = b.getThreadID(evt.RoomID, meta.InReplyTo, meta.References) + meta.ThreadID = b.getThreadID(ctx, evt.RoomID, meta.InReplyTo, meta.References) ctx = threadIDToContext(ctx, meta.ThreadID) } content := evt.Content.AsMessage() @@ -330,16 +330,16 @@ func (b *Bot) SendEmailReply(ctx context.Context) { meta.References = meta.References + " " + meta.MessageID b.log.Info().Any("meta", meta).Msg("sending email reply") eml := email.New(meta.MessageID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, meta.RcptTo, meta.CC, body, htmlBody, nil, nil) - data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey()) + data := eml.Compose(b.cfg.GetBot(ctx).DKIMPrivateKey()) if data == "" { - b.lp.SendNotice(evt.RoomID, "email body is empty", linkpearl.RelatesTo(meta.ThreadID, cfg.NoThreads())) + b.lp.SendNotice(ctx, evt.RoomID, "email body is empty", linkpearl.RelatesTo(meta.ThreadID, cfg.NoThreads())) return } var queued bool recipients := meta.Recipients for _, to := range recipients { - queued, err = b.Sendmail(evt.ID, meta.From, to, data) + queued, err = b.Sendmail(ctx, evt.ID, meta.From, to, data) if queued { b.log.Info().Err(err).Str("from", meta.From).Str("to", to).Msg("email has been queued") b.saveSentMetadata(ctx, queued, meta.ThreadID, recipients, eml, cfg) @@ -444,7 +444,7 @@ func (e *parentEmail) calculateRecipients(from string, forwardedFrom []string) { e.Recipients = rcpts } -func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) { +func (b *Bot) getParentEvent(ctx context.Context, evt *event.Event) (id.EventID, *event.Event) { content := evt.Content.AsMessage() threadID := linkpearl.EventParent(evt.ID, content) b.log.Debug().Str("eventID", evt.ID.String()).Str("threadID", threadID.String()).Msg("looking up for the parent event within thread") @@ -452,23 +452,23 @@ func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) { b.log.Debug().Str("eventID", evt.ID.String()).Msg("event is the thread itself") return threadID, evt } - lastEventID := b.getLastEventID(evt.RoomID, threadID) + lastEventID := b.getLastEventID(ctx, evt.RoomID, threadID) b.log.Debug().Str("eventID", evt.ID.String()).Str("threadID", threadID.String()).Str("lastEventID", lastEventID.String()).Msg("the last event of the thread (and parent of the event) has been found") if lastEventID == evt.ID { return threadID, evt } - parentEvt, err := b.lp.GetClient().GetEvent(evt.RoomID, lastEventID) + parentEvt, err := b.lp.GetClient().GetEvent(ctx, evt.RoomID, lastEventID) if err != nil { b.log.Error().Err(err).Msg("cannot get parent event") return threadID, nil } linkpearl.ParseContent(parentEvt, b.log) - if !b.lp.GetMachine().StateStore.IsEncrypted(evt.RoomID) { + if ok, _ := b.lp.GetMachine().StateStore.IsEncrypted(ctx, evt.RoomID); !ok { //nolint:errcheck // that's fine return threadID, parentEvt } - decrypted, err := b.lp.GetClient().Crypto.Decrypt(parentEvt) + decrypted, err := b.lp.GetClient().Crypto.Decrypt(ctx, parentEvt) if err != nil { b.log.Error().Err(err).Msg("cannot decrypt parent event") return threadID, nil @@ -477,9 +477,9 @@ func (b *Bot) getParentEvent(evt *event.Event) (id.EventID, *event.Event) { return threadID, decrypted } -func (b *Bot) getParentEmail(evt *event.Event, newFromMailbox string) *parentEmail { +func (b *Bot) getParentEmail(ctx context.Context, evt *event.Event, newFromMailbox string) *parentEmail { parent := &parentEmail{} - threadID, parentEvt := b.getParentEvent(evt) + threadID, parentEvt := b.getParentEvent(ctx, evt) parent.ThreadID = threadID if parentEvt == nil { return parent @@ -527,7 +527,7 @@ func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.Eve } evt := eventFromContext(ctx) - content := eml.Content(threadID, cfg.ContentOptions()) + content := eml.Content(threadID, cfg.ContentOptions(), b.psd) notice := format.RenderMarkdown(text, true, true) msgContent, ok := content.Parsed.(*event.MessageEventContent) if !ok { @@ -539,28 +539,28 @@ func (b *Bot) saveSentMetadata(ctx context.Context, queued bool, threadID id.Eve msgContent.FormattedBody = notice.FormattedBody msgContent.RelatesTo = linkpearl.RelatesTo(threadID, cfg.NoThreads()) content.Parsed = msgContent - msgID, err := b.lp.Send(evt.RoomID, content) + msgID, err := b.lp.Send(ctx, evt.RoomID, content) if err != nil { b.Error(ctx, "cannot send notice: %v", err) return } domain := utils.SanitizeDomain(cfg.Domain()) - b.setThreadID(evt.RoomID, email.MessageID(evt.ID, domain), threadID) - b.setThreadID(evt.RoomID, email.MessageID(msgID, domain), threadID) - b.setLastEventID(evt.RoomID, threadID, msgID) + b.setThreadID(ctx, evt.RoomID, email.MessageID(evt.ID, domain), threadID) + b.setThreadID(ctx, evt.RoomID, email.MessageID(msgID, domain), threadID) + b.setLastEventID(ctx, evt.RoomID, threadID, msgID) } func (b *Bot) sendFiles(ctx context.Context, roomID id.RoomID, files []*utils.File, noThreads bool, parentID id.EventID) { for _, file := range files { req := file.Convert() - err := b.lp.SendFile(roomID, req, file.MsgType, linkpearl.RelatesTo(parentID, noThreads)) + err := b.lp.SendFile(ctx, roomID, req, file.MsgType, linkpearl.RelatesTo(parentID, noThreads)) if err != nil { b.Error(ctx, "cannot upload file %s: %v", req.FileName, err) } } } -func (b *Bot) getThreadID(roomID id.RoomID, messageID, references string) id.EventID { +func (b *Bot) getThreadID(ctx context.Context, roomID id.RoomID, messageID, references string) id.EventID { refs := []string{messageID} if references != "" { refs = append(refs, strings.Split(references, " ")...) @@ -568,7 +568,7 @@ func (b *Bot) getThreadID(roomID id.RoomID, messageID, references string) id.Eve for _, refID := range refs { key := acMessagePrefix + "." + refID - data, err := b.lp.GetRoomAccountData(roomID, key) + data, err := b.lp.GetRoomAccountData(ctx, roomID, key) if err != nil { b.log.Error().Err(err).Str("key", key).Msg("cannot retrieve thread ID") continue @@ -576,7 +576,7 @@ func (b *Bot) getThreadID(roomID id.RoomID, messageID, references string) id.Eve if data["eventID"] == "" { continue } - resp, err := b.lp.GetClient().GetEvent(roomID, id.EventID(data["eventID"])) + resp, err := b.lp.GetClient().GetEvent(ctx, roomID, id.EventID(data["eventID"])) if err != nil { b.log.Warn().Err(err).Str("roomID", roomID.String()).Str("eventID", data["eventID"]).Msg("cannot get event by id (may be removed)") continue @@ -587,17 +587,17 @@ func (b *Bot) getThreadID(roomID id.RoomID, messageID, references string) id.Eve return "" } -func (b *Bot) setThreadID(roomID id.RoomID, messageID string, eventID id.EventID) { +func (b *Bot) setThreadID(ctx context.Context, roomID id.RoomID, messageID string, eventID id.EventID) { key := acMessagePrefix + "." + messageID - err := b.lp.SetRoomAccountData(roomID, key, map[string]string{"eventID": eventID.String()}) + err := b.lp.SetRoomAccountData(ctx, roomID, key, map[string]string{"eventID": eventID.String()}) if err != nil { b.log.Error().Err(err).Str("key", key).Msg("cannot save thread ID") } } -func (b *Bot) getLastEventID(roomID id.RoomID, threadID id.EventID) id.EventID { +func (b *Bot) getLastEventID(ctx context.Context, roomID id.RoomID, threadID id.EventID) id.EventID { key := acLastEventPrefix + "." + threadID.String() - data, err := b.lp.GetRoomAccountData(roomID, key) + data, err := b.lp.GetRoomAccountData(ctx, roomID, key) if err != nil { b.log.Error().Err(err).Str("key", key).Msg("cannot retrieve last event ID") return threadID @@ -609,9 +609,9 @@ func (b *Bot) getLastEventID(roomID id.RoomID, threadID id.EventID) id.EventID { return threadID } -func (b *Bot) setLastEventID(roomID id.RoomID, threadID, eventID id.EventID) { +func (b *Bot) setLastEventID(ctx context.Context, roomID id.RoomID, threadID, eventID id.EventID) { key := acLastEventPrefix + "." + threadID.String() - err := b.lp.SetRoomAccountData(roomID, key, map[string]string{"eventID": eventID.String()}) + err := b.lp.SetRoomAccountData(ctx, roomID, key, map[string]string{"eventID": eventID.String()}) if err != nil { b.log.Error().Err(err).Str("key", key).Msg("cannot save thread ID") } diff --git a/bot/mutex.go b/bot/mutex.go index c2daa3e..317c6b8 100644 --- a/bot/mutex.go +++ b/bot/mutex.go @@ -1,29 +1,31 @@ package bot import ( + "context" + "maunium.net/go/mautrix/id" ) -func (b *Bot) lock(roomID id.RoomID, optionalEventID ...id.EventID) { +func (b *Bot) lock(ctx context.Context, roomID id.RoomID, optionalEventID ...id.EventID) { b.mu.Lock(roomID.String()) if len(optionalEventID) == 0 { return } evtID := optionalEventID[0] - if _, err := b.lp.GetClient().SendReaction(roomID, evtID, "📨"); err != nil { + if _, err := b.lp.GetClient().SendReaction(ctx, roomID, evtID, "📨"); err != nil { b.log.Error().Err(err).Str("roomID", roomID.String()).Str("eventID", evtID.String()).Msg("cannot send reaction on lock") } } -func (b *Bot) unlock(roomID id.RoomID, optionalEventID ...id.EventID) { +func (b *Bot) unlock(ctx context.Context, roomID id.RoomID, optionalEventID ...id.EventID) { b.mu.Unlock(roomID.String()) if len(optionalEventID) == 0 { return } evtID := optionalEventID[0] - if _, err := b.lp.GetClient().SendReaction(roomID, evtID, "✅"); err != nil { + if _, err := b.lp.GetClient().SendReaction(ctx, roomID, evtID, "✅"); err != nil { b.log.Error().Err(err).Str("roomID", roomID.String()).Str("eventID", evtID.String()).Msg("cannot send reaction on unlock") } } diff --git a/bot/queue/manager.go b/bot/queue/manager.go index 0486570..286acc2 100644 --- a/bot/queue/manager.go +++ b/bot/queue/manager.go @@ -1,6 +1,8 @@ package queue import ( + "context" + "github.com/rs/zerolog" "gitlab.com/etke.cc/linkpearl" @@ -41,7 +43,8 @@ func (q *Queue) SetSendmail(function func(string, string, string) error) { // Process queue func (q *Queue) Process() { q.log.Debug().Msg("staring queue processing...") - cfg := q.cfg.GetBot() + ctx := context.Background() + cfg := q.cfg.GetBot(ctx) batchSize := cfg.QueueBatch() if batchSize == 0 { @@ -55,7 +58,7 @@ func (q *Queue) Process() { q.mu.Lock(acQueueKey) defer q.mu.Unlock(acQueueKey) - index, err := q.lp.GetAccountData(acQueueKey) + index, err := q.lp.GetAccountData(ctx, acQueueKey) if err != nil { q.log.Error().Err(err).Msg("cannot get queue index") } @@ -66,9 +69,9 @@ func (q *Queue) Process() { q.log.Debug().Msg("finished re-deliveries from queue") return } - if dequeue := q.try(itemkey, maxRetries); dequeue { + if dequeue := q.try(ctx, itemkey, maxRetries); dequeue { q.log.Info().Str("id", id).Msg("email has been delivered") - err = q.Remove(id) + err = q.Remove(ctx, id) if err != nil { q.log.Error().Err(err).Str("id", id).Msg("cannot dequeue email") } diff --git a/bot/queue/queue.go b/bot/queue/queue.go index 92e6b84..50501ee 100644 --- a/bot/queue/queue.go +++ b/bot/queue/queue.go @@ -1,11 +1,12 @@ package queue import ( + "context" "strconv" ) // Add to queue -func (q *Queue) Add(id, from, to, data string) error { +func (q *Queue) Add(ctx context.Context, id, from, to, data string) error { itemkey := acQueueKey + "." + id item := map[string]string{ "attempts": "0", @@ -17,7 +18,7 @@ func (q *Queue) Add(id, from, to, data string) error { q.mu.Lock(itemkey) defer q.mu.Unlock(itemkey) - err := q.lp.SetAccountData(itemkey, item) + err := q.lp.SetAccountData(ctx, itemkey, item) if err != nil { q.log.Error().Err(err).Str("id", id).Msg("cannot enqueue email") return err @@ -25,13 +26,13 @@ func (q *Queue) Add(id, from, to, data string) error { q.mu.Lock(acQueueKey) defer q.mu.Unlock(acQueueKey) - queueIndex, err := q.lp.GetAccountData(acQueueKey) + queueIndex, err := q.lp.GetAccountData(ctx, acQueueKey) if err != nil { q.log.Error().Err(err).Msg("cannot get queue index") return err } queueIndex[id] = itemkey - err = q.lp.SetAccountData(acQueueKey, queueIndex) + err = q.lp.SetAccountData(ctx, acQueueKey, queueIndex) if err != nil { q.log.Error().Err(err).Msg("cannot save queue index") return err @@ -41,8 +42,8 @@ func (q *Queue) Add(id, from, to, data string) error { } // Remove from queue -func (q *Queue) Remove(id string) error { - index, err := q.lp.GetAccountData(acQueueKey) +func (q *Queue) Remove(ctx context.Context, id string) error { + index, err := q.lp.GetAccountData(ctx, acQueueKey) if err != nil { q.log.Error().Err(err).Msg("cannot get queue index") return err @@ -52,7 +53,7 @@ func (q *Queue) Remove(id string) error { itemkey = acQueueKey + "." + id } delete(index, id) - err = q.lp.SetAccountData(acQueueKey, index) + err = q.lp.SetAccountData(ctx, acQueueKey, index) if err != nil { q.log.Error().Err(err).Msg("cannot update queue index") return err @@ -60,15 +61,15 @@ func (q *Queue) Remove(id string) error { q.mu.Lock(itemkey) defer q.mu.Unlock(itemkey) - return q.lp.SetAccountData(itemkey, map[string]string{}) + return q.lp.SetAccountData(ctx, itemkey, map[string]string{}) } // try to send email -func (q *Queue) try(itemkey string, maxRetries int) bool { +func (q *Queue) try(ctx context.Context, itemkey string, maxRetries int) bool { q.mu.Lock(itemkey) defer q.mu.Unlock(itemkey) - item, err := q.lp.GetAccountData(itemkey) + item, err := q.lp.GetAccountData(ctx, itemkey) if err != nil { q.log.Error().Err(err).Str("id", itemkey).Msg("cannot retrieve a queue item") return false @@ -92,7 +93,7 @@ func (q *Queue) try(itemkey string, maxRetries int) bool { q.log.Info().Str("id", itemkey).Str("from", item["from"]).Str("to", item["to"]).Err(err).Msg("attempted to deliver email, but it's not ready yet") attempts++ item["attempts"] = strconv.Itoa(attempts) - err = q.lp.SetAccountData(itemkey, item) + err = q.lp.SetAccountData(ctx, itemkey, item) if err != nil { q.log.Error().Err(err).Str("id", itemkey).Msg("cannot update attempt count on email") } diff --git a/bot/reaction.go b/bot/reaction.go index 186fda5..5f113a0 100644 --- a/bot/reaction.go +++ b/bot/reaction.go @@ -22,14 +22,14 @@ func (b *Bot) handleReaction(ctx context.Context) { } srcID := content.GetRelatesTo().EventID - srcEvt, err := b.lp.GetClient().GetEvent(evt.RoomID, srcID) + srcEvt, err := b.lp.GetClient().GetEvent(ctx, evt.RoomID, srcID) if err != nil { b.Error(ctx, "cannot find event %s: %v", srcID, err) return } linkpearl.ParseContent(srcEvt, b.log) - if b.lp.GetMachine().StateStore.IsEncrypted(evt.RoomID) { - decrypted, derr := b.lp.GetClient().Crypto.Decrypt(srcEvt) + if ok, _ := b.lp.GetMachine().StateStore.IsEncrypted(ctx, evt.RoomID); ok { //nolint:errcheck // that's ok + decrypted, derr := b.lp.GetClient().Crypto.Decrypt(ctx, srcEvt) if derr == nil { srcEvt = decrypted } diff --git a/bot/sync.go b/bot/sync.go index fb72a4b..bfbbb1e 100644 --- a/bot/sync.go +++ b/bot/sync.go @@ -3,7 +3,6 @@ package bot import ( "context" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" ) @@ -12,27 +11,27 @@ func (b *Bot) initSync() { b.lp.OnEventType( event.StateMember, - func(_ mautrix.EventSource, evt *event.Event) { - go b.onMembership(evt) + func(ctx context.Context, evt *event.Event) { + go b.onMembership(ctx, evt) }, ) b.lp.OnEventType( event.EventMessage, - func(_ mautrix.EventSource, evt *event.Event) { - go b.onMessage(evt) + func(ctx context.Context, evt *event.Event) { + go b.onMessage(ctx, evt) }, ) b.lp.OnEventType( event.EventReaction, - func(_ mautrix.EventSource, evt *event.Event) { - go b.onReaction(evt) + func(ctx context.Context, evt *event.Event) { + go b.onReaction(ctx, evt) }, ) } // joinPermit is called by linkpearl when processing "invite" events and deciding if rooms should be auto-joined or not -func (b *Bot) joinPermit(evt *event.Event) bool { - if !b.allowUsers(evt.Sender, evt.RoomID) { +func (b *Bot) joinPermit(ctx context.Context, evt *event.Event) bool { + if !b.allowUsers(ctx, evt.Sender, evt.RoomID) { b.log.Debug().Str("userID", evt.Sender.String()).Msg("Rejecting room invitation from unallowed user") return false } @@ -40,13 +39,13 @@ func (b *Bot) joinPermit(evt *event.Event) bool { return true } -func (b *Bot) onMembership(evt *event.Event) { +func (b *Bot) onMembership(ctx context.Context, evt *event.Event) { // mautrix 0.15.x migration if b.ignoreBefore >= evt.Timestamp { return } - ctx := newContext(evt) + ctx = newContext(ctx, evt) evtType := evt.Content.AsMember().Membership if evtType == event.MembershipJoin && evt.Sender == b.lp.GetClient().UserID { @@ -61,7 +60,7 @@ func (b *Bot) onMembership(evt *event.Event) { // Potentially handle other membership events in the future } -func (b *Bot) onMessage(evt *event.Event) { +func (b *Bot) onMessage(ctx context.Context, evt *event.Event) { // ignore own messages if evt.Sender == b.lp.GetClient().UserID { return @@ -71,11 +70,11 @@ func (b *Bot) onMessage(evt *event.Event) { return } - ctx := newContext(evt) + ctx = newContext(ctx, evt) b.handle(ctx) } -func (b *Bot) onReaction(evt *event.Event) { +func (b *Bot) onReaction(ctx context.Context, evt *event.Event) { // ignore own messages if evt.Sender == b.lp.GetClient().UserID { return @@ -85,7 +84,7 @@ func (b *Bot) onReaction(evt *event.Event) { return } - ctx := newContext(evt) + ctx = newContext(ctx, evt) b.handleReaction(ctx) } @@ -100,7 +99,7 @@ func (b *Bot) onBotJoin(ctx context.Context) { return } - b.sendIntroduction(evt.RoomID) + b.sendIntroduction(ctx, evt.RoomID) b.sendHelp(ctx) } @@ -111,7 +110,7 @@ func (b *Bot) onLeave(ctx context.Context) { b.log.Info().Str("eventID", evt.ID.String()).Msg("Suppressing already handled event") return } - members, err := b.lp.GetClient().StateStore.GetRoomJoinedOrInvitedMembers(evt.RoomID) + members, err := b.lp.GetClient().StateStore.GetRoomJoinedOrInvitedMembers(ctx, evt.RoomID) if err != nil { b.log.Error().Err(err).Str("roomID", evt.RoomID.String()).Msg("cannot get joined or invited members") return @@ -121,7 +120,7 @@ func (b *Bot) onLeave(ctx context.Context) { if count == 1 && members[0] == b.lp.GetClient().UserID { b.log.Info().Str("roomID", evt.RoomID.String()).Msg("no more users left in the room") b.runStop(ctx) - _, err := b.lp.GetClient().LeaveRoom(evt.RoomID) + _, err := b.lp.GetClient().LeaveRoom(ctx, evt.RoomID) if err != nil { b.Error(ctx, "cannot leave empty room: %v", err) } diff --git a/cmd/cmd.go b/cmd/cmd.go index aafc72b..259a807 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -114,9 +114,10 @@ func initMatrix(cfg *config.Config) { log.Fatal().Err(err).Msg("cannot initialize matrix bot") } + psd := utils.NewPSD(cfg.PSD.URL, cfg.PSD.Login, cfg.PSD.Password, &log) mxc = mxconfig.New(lp, &log) q = queue.New(lp, mxc, &log) - mxb, err = bot.New(q, lp, &log, mxc, cfg.Proxies, cfg.Prefix, cfg.Domains, cfg.Admins, bot.MBXConfig(cfg.Mailboxes)) + mxb, err = bot.New(q, lp, &log, mxc, psd, cfg.Proxies, cfg.Prefix, cfg.Domains, cfg.Admins, bot.MBXConfig(cfg.Mailboxes)) if err != nil { log.Panic().Err(err).Msg("cannot start matrix bot") } diff --git a/config/config.go b/config/config.go index 2415445..d857878 100644 --- a/config/config.go +++ b/config/config.go @@ -48,6 +48,11 @@ func New() *Config { DSN: env.String("db.dsn", defaultConfig.DB.DSN), Dialect: env.String("db.dialect", defaultConfig.DB.Dialect), }, + PSD: PSD{ + URL: env.String("psd.url"), + Login: env.String("psd.login"), + Password: env.String("psd.password"), + }, Relay: Relay{ Host: env.String("relay.host", defaultConfig.Relay.Host), Port: env.String("relay.port", defaultConfig.Relay.Port), diff --git a/config/types.go b/config/types.go index 191e818..01c9d55 100644 --- a/config/types.go +++ b/config/types.go @@ -38,6 +38,9 @@ type Config struct { // DB config DB DB + // PSD config + PSD PSD + // TLS config TLS TLS @@ -78,6 +81,12 @@ type Mailboxes struct { Activation string } +type PSD struct { + URL string + Login string + Password string +} + // Relay config type Relay struct { Host string diff --git a/email/email.go b/email/email.go index b4ad425..85fc240 100644 --- a/email/email.go +++ b/email/email.go @@ -108,8 +108,9 @@ func (e *Email) Mailbox(incoming bool) string { return utils.Mailbox(e.From) } -func (e *Email) contentHeader(threadID id.EventID, text *strings.Builder, options *ContentOptions) { +func (e *Email) contentHeader(threadID id.EventID, text *strings.Builder, options *ContentOptions, psd *utils.PSD) { if options.Sender { + text.WriteString(psd.Status(e.From)) text.WriteString(e.From) } if options.Recipient { @@ -125,8 +126,12 @@ func (e *Email) contentHeader(threadID id.EventID, text *strings.Builder, option } } if options.CC && len(e.CC) > 0 { + ccs := make([]string, 0, len(e.CC)) + for _, addr := range e.CC { + ccs = append(ccs, psd.Status(addr)+addr) + } text.WriteString("\ncc: ") - text.WriteString(strings.Join(e.CC, ", ")) + text.WriteString(strings.Join(ccs, ", ")) } if options.Sender || options.Recipient || options.CC { text.WriteString("\n\n") @@ -146,10 +151,10 @@ func (e *Email) contentHeader(threadID id.EventID, text *strings.Builder, option } // Content converts the email object to a Matrix event content -func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Content { +func (e *Email) Content(threadID id.EventID, options *ContentOptions, psd *utils.PSD) *event.Content { var text strings.Builder - e.contentHeader(threadID, &text, options) + e.contentHeader(threadID, &text, options, psd) if threadID != "" || (threadID == "" && !options.Threadify) { if e.HTML != "" && options.HTML { diff --git a/go.mod b/go.mod index 36ecac5..c6486a5 100644 --- a/go.mod +++ b/go.mod @@ -18,16 +18,16 @@ require ( github.com/mcnijman/go-emailaddress v1.1.0 github.com/mileusna/crontab v1.2.0 github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 - github.com/rs/zerolog v1.31.0 - gitlab.com/etke.cc/go/env v1.0.0 + github.com/rs/zerolog v1.32.0 + gitlab.com/etke.cc/go/env v1.1.0 gitlab.com/etke.cc/go/fswatcher v1.0.0 gitlab.com/etke.cc/go/healthchecks v1.0.1 gitlab.com/etke.cc/go/mxidwc v1.0.0 gitlab.com/etke.cc/go/secgen v1.1.1 gitlab.com/etke.cc/go/validator v1.0.6 - gitlab.com/etke.cc/linkpearl v0.0.0-20231121221431-72443f33d266 - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - maunium.net/go/mautrix v0.16.2 + gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a + golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 + maunium.net/go/mautrix v0.17.0 ) require ( @@ -50,12 +50,12 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/yuin/goldmark v1.6.0 // indirect + github.com/yuin/goldmark v1.7.0 // indirect gitlab.com/etke.cc/go/trysmtp v1.1.3 // indirect - go.mau.fi/util v0.2.1 // indirect - golang.org/x/crypto v0.15.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect + go.mau.fi/util v0.3.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect maunium.net/go/maulogger/v2 v2.4.1 // indirect ) diff --git a/go.sum b/go.sum index 6fc7b1d..2a8b157 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ blitiri.com.ar/go/spf v1.5.1 h1:CWUEasc44OrANJD8CzceRnRn1Jv0LttY68cYym2/pbE= blitiri.com.ar/go/spf v1.5.1/go.mod h1:E71N92TfL4+Yyd5lpKuE9CAF2pd4JrUq1xQfkTxoNdk= -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.1 h1:FK6RCIUSfmbnI/imIICmboyQBkOckutaa6R5YYlLZyo= github.com/archdx/zerolog-sentry v1.2.0 h1:FDFqlo5XvL/jpDAPoAWI15EjJQVFvixn70v3IH//eTM= github.com/archdx/zerolog-sentry v1.2.0/go.mod h1:3H8gClGFafB90fKMsvfP017bdmkG5MD6UiA+6iPEwGw= github.com/buger/jsonparser v1.0.0 h1:etJTGF5ESxjI0Ic2UaLQs2LQQpa8G9ykQScukbh4L8A= @@ -55,8 +55,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= -github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mcnijman/go-emailaddress v1.1.0 h1:7/Uxgn9pXwXmvXsFSgORo6XoRTrttj7AGmmB2yFArAg= @@ -78,8 +76,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -96,10 +94,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= -github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= +github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= gitlab.com/etke.cc/go/env v1.0.0 h1:J98BwzOuELnjsVPFvz5wa79L7IoRV9CmrS41xLYXtSw= gitlab.com/etke.cc/go/env v1.0.0/go.mod h1:e1l4RM5MA1sc0R1w/RBDAESWRwgo5cOG9gx8BKUn2C4= +gitlab.com/etke.cc/go/env v1.1.0 h1:nbMhZkMu6C8lysRlb5siIiylWuyVkGAgEvwWEqz/82o= +gitlab.com/etke.cc/go/env v1.1.0/go.mod h1:e1l4RM5MA1sc0R1w/RBDAESWRwgo5cOG9gx8BKUn2C4= gitlab.com/etke.cc/go/fswatcher v1.0.0 h1:uyiVn+1NVCjOLZrXSZouIDBDZBMwVipS4oYuvAFpPzo= gitlab.com/etke.cc/go/fswatcher v1.0.0/go.mod h1:MqTOxyhXfvaVZQUL9/Ksbl2ow1PTBVu3eqIldvMq0RE= gitlab.com/etke.cc/go/healthchecks v1.0.1 h1:IxPB+r4KtEM6wf4K7MeQoH1XnuBITMGUqFaaRIgxeUY= @@ -112,21 +112,21 @@ gitlab.com/etke.cc/go/trysmtp v1.1.3 h1:e2EHond77onMaecqCg6mWumffTSEf+ycgj88nbee gitlab.com/etke.cc/go/trysmtp v1.1.3/go.mod h1:lOO7tTdAE0a3ETV3wN3GJ7I1Tqewu7YTpPWaOmTteV0= gitlab.com/etke.cc/go/validator v1.0.6 h1:w0Muxf9Pqw7xvF7NaaswE6d7r9U3nB2t2l5PnFMrecQ= gitlab.com/etke.cc/go/validator v1.0.6/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts= -gitlab.com/etke.cc/linkpearl v0.0.0-20231121221431-72443f33d266 h1:mGbLQkdE35WeyinqP38HC0dqUOJ7FItEAumVIOz7Gg8= -gitlab.com/etke.cc/linkpearl v0.0.0-20231121221431-72443f33d266/go.mod h1:wFEvngglb6ZTlE58/2a9gwYYs6V3FTYclYn5Pf5EGyQ= -go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw= -go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c= +gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a h1:30WtX+uepGqyFnU7jIockJWxQUeYdljhhk63DCOXLZs= +gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a/go.mod h1:3lqQGDDtk52Jm8PD3mZ3qhmIp4JXuq95waWH5vmEacc= +go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs= +go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs= golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= +golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -135,11 +135,11 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -152,5 +152,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= -maunium.net/go/mautrix v0.16.2 h1:a6GUJXNWsTEOO8VE4dROBfCIfPp50mqaqzv7KPzChvg= -maunium.net/go/mautrix v0.16.2/go.mod h1:YL4l4rZB46/vj/ifRMEjcibbvHjgxHftOF1SgmruLu4= +maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8= +maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8= diff --git a/smtp/listener.go b/smtp/listener.go index 81018b3..e629202 100644 --- a/smtp/listener.go +++ b/smtp/listener.go @@ -1,6 +1,7 @@ package smtp import ( + "context" "crypto/tls" "net" "sync" @@ -15,10 +16,10 @@ type Listener struct { tls *tls.Config tlsMu sync.Mutex listener net.Listener - isBanned func(net.Addr) bool + isBanned func(context.Context, net.Addr) bool } -func NewListener(port string, tlsConfig *tls.Config, isBanned func(net.Addr) bool, log *zerolog.Logger) (*Listener, error) { +func NewListener(port string, tlsConfig *tls.Config, isBanned func(context.Context, net.Addr) bool, log *zerolog.Logger) (*Listener, error) { actual, err := net.Listen("tcp", ":"+port) if err != nil { return nil, err @@ -52,7 +53,7 @@ func (l *Listener) Accept() (net.Conn, error) { continue } } - if l.isBanned(conn.RemoteAddr()) { + if l.isBanned(context.Background(), conn.RemoteAddr()) { conn.Close() l.log.Info().Str("addr", conn.RemoteAddr().String()).Msg("rejected connection (already banned)") continue diff --git a/smtp/manager.go b/smtp/manager.go index 8d6175a..e973357 100644 --- a/smtp/manager.go +++ b/smtp/manager.go @@ -60,16 +60,16 @@ type Manager struct { } type matrixbot interface { - AllowAuth(string, string) (id.RoomID, bool) - IsGreylisted(net.Addr) bool - IsBanned(net.Addr) bool + AllowAuth(context.Context, string, string) (id.RoomID, bool) + IsGreylisted(context.Context, net.Addr) bool + IsBanned(context.Context, net.Addr) bool IsTrusted(net.Addr) bool - BanAuto(net.Addr) - BanAuth(net.Addr) - GetMapping(string) (id.RoomID, bool) - GetIFOptions(id.RoomID) email.IncomingFilteringOptions + BanAuto(context.Context, net.Addr) + BanAuth(context.Context, net.Addr) + GetMapping(context.Context, string) (id.RoomID, bool) + GetIFOptions(context.Context, id.RoomID) email.IncomingFilteringOptions IncomingEmail(context.Context, *email.Email) error - GetDKIMprivkey() string + GetDKIMprivkey(context.Context) string } // Caller is Sendmail caller diff --git a/smtp/server.go b/smtp/server.go index ac6fb91..fcaf3fa 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -46,27 +46,28 @@ type mailServer struct { // Login used for outgoing mail submissions only (when you use postmoogle as smtp server in your scripts) func (m *mailServer) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { m.log.Debug().Str("username", username).Any("state", state).Msg("Login") - if m.bot.IsBanned(state.RemoteAddr) { + ctx := context.Background() + if m.bot.IsBanned(ctx, state.RemoteAddr) { return nil, ErrBanned } if !email.AddressValid(username) { m.log.Debug().Str("address", username).Msg("address is invalid") - m.bot.BanAuth(state.RemoteAddr) + m.bot.BanAuth(ctx, state.RemoteAddr) return nil, ErrBanned } - roomID, allow := m.bot.AllowAuth(username, password) + roomID, allow := m.bot.AllowAuth(ctx, username, password) if !allow { m.log.Debug().Str("username", username).Msg("username or password is invalid") - m.bot.BanAuth(state.RemoteAddr) + m.bot.BanAuth(ctx, state.RemoteAddr) return nil, ErrBanned } return &outgoingSession{ ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()), sendmail: m.sender.Send, - privkey: m.bot.GetDKIMprivkey(), + privkey: m.bot.GetDKIMprivkey(ctx), from: username, log: m.log, domains: m.domains, @@ -79,7 +80,8 @@ func (m *mailServer) Login(state *smtp.ConnectionState, username, password strin // AnonymousLogin used for incoming mail submissions only func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { m.log.Debug().Any("state", state).Msg("AnonymousLogin") - if m.bot.IsBanned(state.RemoteAddr) { + ctx := context.Background() + if m.bot.IsBanned(ctx, state.RemoteAddr) { return nil, ErrBanned } diff --git a/smtp/session.go b/smtp/session.go index c8a1d12..40cab0c 100644 --- a/smtp/session.go +++ b/smtp/session.go @@ -33,12 +33,12 @@ var ( // incomingSession represents an SMTP-submission session receiving emails from remote servers type incomingSession struct { log *zerolog.Logger - getRoomID func(string) (id.RoomID, bool) - getFilters func(id.RoomID) email.IncomingFilteringOptions + getRoomID func(context.Context, string) (id.RoomID, bool) + getFilters func(context.Context, id.RoomID) email.IncomingFilteringOptions receiveEmail func(context.Context, *email.Email) error - greylisted func(net.Addr) bool + greylisted func(context.Context, net.Addr) bool trusted func(net.Addr) bool - ban func(net.Addr) + ban func(context.Context, net.Addr) domains []string roomID id.RoomID @@ -52,7 +52,7 @@ func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error { sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from) if !email.AddressValid(from) { s.log.Debug().Str("from", from).Msg("address is invalid") - s.ban(s.addr) + s.ban(s.ctx, s.addr) return ErrBanned } s.from = email.Address(from) @@ -77,7 +77,7 @@ func (s *incomingSession) Rcpt(to string) error { } var ok bool - s.roomID, ok = s.getRoomID(utils.Mailbox(to)) + s.roomID, ok = s.getRoomID(s.ctx, utils.Mailbox(to)) if !ok { s.log.Debug().Str("to", to).Msg("mapping not found") return ErrNoUser @@ -126,12 +126,12 @@ func (s *incomingSession) Data(r io.Reader) error { } addr := s.getAddr(envelope) reader.Seek(0, io.SeekStart) //nolint:errcheck // becase we're sure that's ok - validations := s.getFilters(s.roomID) + validations := s.getFilters(s.ctx, s.roomID) if !validateIncoming(s.from, s.tos[0], addr, s.log, validations) { - s.ban(addr) + s.ban(s.ctx, addr) return ErrBanned } - if s.greylisted(addr) { + if s.greylisted(s.ctx, addr) { return &smtp.SMTPError{ Code: GraylistCode, EnhancedCode: GraylistEnhancedCode, @@ -172,7 +172,7 @@ type outgoingSession struct { sendmail func(string, string, string) error privkey string domains []string - getRoomID func(string) (id.RoomID, bool) + getRoomID func(context.Context, string) (id.RoomID, bool) ctx context.Context //nolint:containedctx // that's session tos []string @@ -198,7 +198,7 @@ func (s *outgoingSession) Mail(from string, _ smtp.MailOptions) error { return ErrNoUser } - roomID, ok := s.getRoomID(utils.Mailbox(from)) + roomID, ok := s.getRoomID(s.ctx, utils.Mailbox(from)) if !ok { s.log.Debug().Str("from", from).Msg("mapping not found") return ErrNoUser diff --git a/utils/psd.go b/utils/psd.go new file mode 100644 index 0000000..a731231 --- /dev/null +++ b/utils/psd.go @@ -0,0 +1,112 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "sync" + "time" + + "github.com/rs/zerolog" +) + +//nolint:gocritic // sync.Mutex is intended +type PSD struct { + sync.Mutex + cachedAt time.Time + cache map[string]bool + log *zerolog.Logger + url *url.URL + login string + password string +} + +type PSDTarget struct { + Targets []string `json:"targets"` + Labels map[string]string `json:"labels"` +} + +func NewPSD(baseURL, login, password string, log *zerolog.Logger) *PSD { + uri, err := url.Parse(baseURL) + if err != nil || login == "" || password == "" { + return &PSD{} + } + return &PSD{url: uri, login: login, password: password, log: log} +} + +func (p *PSD) Contains(email string) (bool, error) { + if p.cachedAt.IsZero() || time.Since(p.cachedAt) > 10*time.Minute { + err := p.updateCache() + if err != nil { + return false, err + } + } + + p.Lock() + defer p.Unlock() + return p.cache[email], nil +} + +func (p *PSD) Status(email string) string { + ok, err := p.Contains(email) + if !ok || err != nil { + return "" + } + return "👤" +} + +func (p *PSD) updateCache() error { + p.Lock() + defer p.Unlock() + defer func() { + p.cachedAt = time.Now() + }() + + if p.url == nil { + return nil + } + cloned := *p.url + uri := cloned.JoinPath("/emails") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), http.NoBody) + if err != nil { + p.log.Error().Err(err).Msg("failed to create request") + return err + } + req.SetBasicAuth(p.login, p.password) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("%s", resp.Status) //nolint:goerr113 // no need to wrap + p.log.Error().Err(err).Msg("failed to fetch PSD") + return err + } + datab, err := io.ReadAll(resp.Body) + if err != nil { + p.log.Error().Err(err).Msg("failed to read response") + return err + } + var psd []*PSDTarget + err = json.Unmarshal(datab, &psd) + if err != nil { + p.log.Error().Err(err).Msg("failed to unmarshal response") + return err + } + + p.cache = make(map[string]bool) + for _, t := range psd { + for _, email := range t.Targets { + p.cache[email] = true + } + } + + return nil +} diff --git a/vendor/github.com/rs/zerolog/README.md b/vendor/github.com/rs/zerolog/README.md index 972b729..afa6156 100644 --- a/vendor/github.com/rs/zerolog/README.md +++ b/vendor/github.com/rs/zerolog/README.md @@ -547,7 +547,7 @@ and facilitates the unification of logging and tracing in some systems: type TracingHook struct{} func (h TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { - ctx := e.Ctx() + ctx := e.GetCtx() spanId := getSpanIdFromContext(ctx) // as per your tracing framework e.Str("span-id", spanId) } diff --git a/vendor/github.com/rs/zerolog/console.go b/vendor/github.com/rs/zerolog/console.go index 2827988..cc6d623 100644 --- a/vendor/github.com/rs/zerolog/console.go +++ b/vendor/github.com/rs/zerolog/console.go @@ -76,6 +76,8 @@ type ConsoleWriter struct { FormatErrFieldValue Formatter FormatExtra func(map[string]interface{}, *bytes.Buffer) error + + FormatPrepare func(map[string]interface{}) error } // NewConsoleWriter creates and initializes a new ConsoleWriter. @@ -124,6 +126,13 @@ func (w ConsoleWriter) Write(p []byte) (n int, err error) { return n, fmt.Errorf("cannot decode event: %s", err) } + if w.FormatPrepare != nil { + err = w.FormatPrepare(evt) + if err != nil { + return n, err + } + } + for _, p := range w.PartsOrder { w.writePart(buf, evt, p) } @@ -146,6 +155,15 @@ func (w ConsoleWriter) Write(p []byte) (n int, err error) { return len(p), err } +// Call the underlying writer's Close method if it is an io.Closer. Otherwise +// does nothing. +func (w ConsoleWriter) Close() error { + if closer, ok := w.Out.(io.Closer); ok { + return closer.Close() + } + return nil +} + // writeFields appends formatted key-value pairs to buf. func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer) { var fields = make([]string, 0, len(evt)) @@ -272,7 +290,7 @@ func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt map[string]interface{}, } case MessageFieldName: if w.FormatMessage == nil { - f = consoleDefaultFormatMessage + f = consoleDefaultFormatMessage(w.NoColor, evt[LevelFieldName]) } else { f = w.FormatMessage } @@ -310,10 +328,10 @@ func needsQuote(s string) bool { return false } -// colorize returns the string s wrapped in ANSI code c, unless disabled is true. +// colorize returns the string s wrapped in ANSI code c, unless disabled is true or c is 0. func colorize(s interface{}, c int, disabled bool) string { e := os.Getenv("NO_COLOR") - if e != "" { + if e != "" || c == 0 { disabled = true } @@ -378,27 +396,16 @@ func consoleDefaultFormatLevel(noColor bool) Formatter { return func(i interface{}) string { var l string if ll, ok := i.(string); ok { - switch ll { - case LevelTraceValue: - l = colorize("TRC", colorMagenta, noColor) - case LevelDebugValue: - l = colorize("DBG", colorYellow, noColor) - case LevelInfoValue: - l = colorize("INF", colorGreen, noColor) - case LevelWarnValue: - l = colorize("WRN", colorRed, noColor) - case LevelErrorValue: - l = colorize(colorize("ERR", colorRed, noColor), colorBold, noColor) - case LevelFatalValue: - l = colorize(colorize("FTL", colorRed, noColor), colorBold, noColor) - case LevelPanicValue: - l = colorize(colorize("PNC", colorRed, noColor), colorBold, noColor) - default: - l = colorize(ll, colorBold, noColor) + level, _ := ParseLevel(ll) + fl, ok := FormattedLevels[level] + if ok { + l = colorize(fl, LevelColors[level], noColor) + } else { + l = strings.ToUpper(ll)[0:3] } } else { if i == nil { - l = colorize("???", colorBold, noColor) + l = "???" } else { l = strings.ToUpper(fmt.Sprintf("%s", i))[0:3] } @@ -425,11 +432,18 @@ func consoleDefaultFormatCaller(noColor bool) Formatter { } } -func consoleDefaultFormatMessage(i interface{}) string { - if i == nil { - return "" +func consoleDefaultFormatMessage(noColor bool, level interface{}) Formatter { + return func(i interface{}) string { + if i == nil || i == "" { + return "" + } + switch level { + case LevelInfoValue, LevelWarnValue, LevelErrorValue, LevelFatalValue, LevelPanicValue: + return colorize(fmt.Sprintf("%s", i), colorBold, noColor) + default: + return fmt.Sprintf("%s", i) + } } - return fmt.Sprintf("%s", i) } func consoleDefaultFormatFieldName(noColor bool) Formatter { @@ -450,6 +464,6 @@ func consoleDefaultFormatErrFieldName(noColor bool) Formatter { func consoleDefaultFormatErrFieldValue(noColor bool) Formatter { return func(i interface{}) string { - return colorize(fmt.Sprintf("%s", i), colorRed, noColor) + return colorize(colorize(fmt.Sprintf("%s", i), colorBold, noColor), colorRed, noColor) } } diff --git a/vendor/github.com/rs/zerolog/context.go b/vendor/github.com/rs/zerolog/context.go index fc62ad9..df352eb 100644 --- a/vendor/github.com/rs/zerolog/context.go +++ b/vendor/github.com/rs/zerolog/context.go @@ -3,7 +3,7 @@ package zerolog import ( "context" "fmt" - "io/ioutil" + "io" "math" "net" "time" @@ -23,7 +23,7 @@ func (c Context) Logger() Logger { // Only map[string]interface{} and []interface{} are accepted. []interface{} must // alternate string keys and arbitrary values, and extraneous ones are ignored. func (c Context) Fields(fields interface{}) Context { - c.l.context = appendFields(c.l.context, fields) + c.l.context = appendFields(c.l.context, fields, c.l.stack) return c } @@ -57,7 +57,7 @@ func (c Context) Array(key string, arr LogArrayMarshaler) Context { // Object marshals an object that implement the LogObjectMarshaler interface. func (c Context) Object(key string, obj LogObjectMarshaler) Context { - e := newEvent(LevelWriterAdapter{ioutil.Discard}, 0) + e := newEvent(LevelWriterAdapter{io.Discard}, 0) e.Object(key, obj) c.l.context = enc.AppendObjectData(c.l.context, e.buf) putEvent(e) @@ -66,7 +66,7 @@ func (c Context) Object(key string, obj LogObjectMarshaler) Context { // EmbedObject marshals and Embeds an object that implement the LogObjectMarshaler interface. func (c Context) EmbedObject(obj LogObjectMarshaler) Context { - e := newEvent(LevelWriterAdapter{ioutil.Discard}, 0) + e := newEvent(LevelWriterAdapter{io.Discard}, 0) e.EmbedObject(obj) c.l.context = enc.AppendObjectData(c.l.context, e.buf) putEvent(e) @@ -163,6 +163,22 @@ func (c Context) Errs(key string, errs []error) Context { // Err adds the field "error" with serialized err to the logger context. func (c Context) Err(err error) Context { + if c.l.stack && ErrorStackMarshaler != nil { + switch m := ErrorStackMarshaler(err).(type) { + case nil: + case LogObjectMarshaler: + c = c.Object(ErrorStackFieldName, m) + case error: + if m != nil && !isNilValue(m) { + c = c.Str(ErrorStackFieldName, m.Error()) + } + case string: + c = c.Str(ErrorStackFieldName, m) + default: + c = c.Interface(ErrorStackFieldName, m) + } + } + return c.AnErr(ErrorFieldName, err) } @@ -375,10 +391,19 @@ func (c Context) Durs(key string, d []time.Duration) Context { // Interface adds the field key with obj marshaled using reflection. func (c Context) Interface(key string, i interface{}) Context { + if obj, ok := i.(LogObjectMarshaler); ok { + return c.Object(key, obj) + } c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), i) return c } +// Type adds the field key with val's type using reflection. +func (c Context) Type(key string, val interface{}) Context { + c.l.context = enc.AppendType(enc.AppendKey(c.l.context, key), val) + return c +} + // Any is a wrapper around Context.Interface. func (c Context) Any(key string, i interface{}) Context { return c.Interface(key, i) diff --git a/vendor/github.com/rs/zerolog/event.go b/vendor/github.com/rs/zerolog/event.go index 2a5d3b0..5c949f8 100644 --- a/vendor/github.com/rs/zerolog/event.go +++ b/vendor/github.com/rs/zerolog/event.go @@ -164,7 +164,7 @@ func (e *Event) Fields(fields interface{}) *Event { if e == nil { return e } - e.buf = appendFields(e.buf, fields) + e.buf = appendFields(e.buf, fields, e.stack) return e } diff --git a/vendor/github.com/rs/zerolog/example.jsonl b/vendor/github.com/rs/zerolog/example.jsonl new file mode 100644 index 0000000..d73193d --- /dev/null +++ b/vendor/github.com/rs/zerolog/example.jsonl @@ -0,0 +1,7 @@ +{"time":"5:41PM","level":"info","message":"Starting listener","listen":":8080","pid":37556} +{"time":"5:41PM","level":"debug","message":"Access","database":"myapp","host":"localhost:4962","pid":37556} +{"time":"5:41PM","level":"info","message":"Access","method":"GET","path":"/users","pid":37556,"resp_time":23} +{"time":"5:41PM","level":"info","message":"Access","method":"POST","path":"/posts","pid":37556,"resp_time":532} +{"time":"5:41PM","level":"warn","message":"Slow request","method":"POST","path":"/posts","pid":37556,"resp_time":532} +{"time":"5:41PM","level":"info","message":"Access","method":"GET","path":"/users","pid":37556,"resp_time":10} +{"time":"5:41PM","level":"error","message":"Database connection lost","database":"myapp","pid":37556,"error":"connection reset by peer"} diff --git a/vendor/github.com/rs/zerolog/fields.go b/vendor/github.com/rs/zerolog/fields.go index c1eb5ce..23606dd 100644 --- a/vendor/github.com/rs/zerolog/fields.go +++ b/vendor/github.com/rs/zerolog/fields.go @@ -12,13 +12,13 @@ func isNilValue(i interface{}) bool { return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0 } -func appendFields(dst []byte, fields interface{}) []byte { +func appendFields(dst []byte, fields interface{}, stack bool) []byte { switch fields := fields.(type) { case []interface{}: if n := len(fields); n&0x1 == 1 { // odd number fields = fields[:n-1] } - dst = appendFieldList(dst, fields) + dst = appendFieldList(dst, fields, stack) case map[string]interface{}: keys := make([]string, 0, len(fields)) for key := range fields { @@ -28,13 +28,13 @@ func appendFields(dst []byte, fields interface{}) []byte { kv := make([]interface{}, 2) for _, key := range keys { kv[0], kv[1] = key, fields[key] - dst = appendFieldList(dst, kv) + dst = appendFieldList(dst, kv, stack) } } return dst } -func appendFieldList(dst []byte, kvList []interface{}) []byte { +func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte { for i, n := 0, len(kvList); i < n; i += 2 { key, val := kvList[i], kvList[i+1] if key, ok := key.(string); ok { @@ -74,6 +74,21 @@ func appendFieldList(dst []byte, kvList []interface{}) []byte { default: dst = enc.AppendInterface(dst, m) } + + if stack && ErrorStackMarshaler != nil { + dst = enc.AppendKey(dst, ErrorStackFieldName) + switch m := ErrorStackMarshaler(val).(type) { + case nil: + case error: + if m != nil && !isNilValue(m) { + dst = enc.AppendString(dst, m.Error()) + } + case string: + dst = enc.AppendString(dst, m) + default: + dst = enc.AppendInterface(dst, m) + } + } case []error: dst = enc.AppendArrayStart(dst) for i, err := range val { diff --git a/vendor/github.com/rs/zerolog/globals.go b/vendor/github.com/rs/zerolog/globals.go index e1067de..b38a7fc 100644 --- a/vendor/github.com/rs/zerolog/globals.go +++ b/vendor/github.com/rs/zerolog/globals.go @@ -108,6 +108,34 @@ var ( // DefaultContextLogger is returned from Ctx() if there is no logger associated // with the context. DefaultContextLogger *Logger + + // LevelColors are used by ConsoleWriter's consoleDefaultFormatLevel to color + // log levels. + LevelColors = map[Level]int{ + TraceLevel: colorBlue, + DebugLevel: 0, + InfoLevel: colorGreen, + WarnLevel: colorYellow, + ErrorLevel: colorRed, + FatalLevel: colorRed, + PanicLevel: colorRed, + } + + // FormattedLevels are used by ConsoleWriter's consoleDefaultFormatLevel + // for a short level name. + FormattedLevels = map[Level]string{ + TraceLevel: "TRC", + DebugLevel: "DBG", + InfoLevel: "INF", + WarnLevel: "WRN", + ErrorLevel: "ERR", + FatalLevel: "FTL", + PanicLevel: "PNC", + } + + // TriggerLevelWriterBufferReuseLimit is a limit in bytes that a buffer is dropped + // from the TriggerLevelWriter buffer pool if the buffer grows above the limit. + TriggerLevelWriterBufferReuseLimit = 64 * 1024 ) var ( diff --git a/vendor/github.com/rs/zerolog/log.go b/vendor/github.com/rs/zerolog/log.go index 834c7e6..9fec7cc 100644 --- a/vendor/github.com/rs/zerolog/log.go +++ b/vendor/github.com/rs/zerolog/log.go @@ -118,7 +118,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "strconv" "strings" @@ -246,7 +245,7 @@ type Logger struct { // you may consider using sync wrapper. func New(w io.Writer) Logger { if w == nil { - w = ioutil.Discard + w = io.Discard } lw, ok := w.(LevelWriter) if !ok { @@ -326,10 +325,13 @@ func (l Logger) Sample(s Sampler) Logger { } // Hook returns a logger with the h Hook. -func (l Logger) Hook(h Hook) Logger { - newHooks := make([]Hook, len(l.hooks), len(l.hooks)+1) +func (l Logger) Hook(hooks ...Hook) Logger { + if len(hooks) == 0 { + return l + } + newHooks := make([]Hook, len(l.hooks), len(l.hooks)+len(hooks)) copy(newHooks, l.hooks) - l.hooks = append(newHooks, h) + l.hooks = append(newHooks, hooks...) return l } @@ -385,7 +387,14 @@ func (l *Logger) Err(err error) *Event { // // You must call Msg on the returned event in order to send the event. func (l *Logger) Fatal() *Event { - return l.newEvent(FatalLevel, func(msg string) { os.Exit(1) }) + return l.newEvent(FatalLevel, func(msg string) { + if closer, ok := l.w.(io.Closer); ok { + // Close the writer to flush any buffered message. Otherwise the message + // will be lost as os.Exit() terminates the program immediately. + closer.Close() + } + os.Exit(1) + }) } // Panic starts a new message with panic level. The panic() function @@ -450,6 +459,14 @@ func (l *Logger) Printf(format string, v ...interface{}) { } } +// Println sends a log event using debug level and no extra field. +// Arguments are handled in the manner of fmt.Println. +func (l *Logger) Println(v ...interface{}) { + if e := l.Debug(); e.Enabled() { + e.CallerSkipFrame(1).Msg(fmt.Sprintln(v...)) + } +} + // Write implements the io.Writer interface. This is useful to set as a writer // for the standard library log. func (l Logger) Write(p []byte) (n int, err error) { @@ -488,6 +505,9 @@ func (l *Logger) newEvent(level Level, done func(string)) *Event { // should returns true if the log event should be logged. func (l *Logger) should(lvl Level) bool { + if l.w == nil { + return false + } if lvl < l.level || lvl < GlobalLevel() { return false } diff --git a/vendor/github.com/rs/zerolog/pretty.png b/vendor/github.com/rs/zerolog/pretty.png index 242033686debf04c5b1756c166d05d09767594e3..1449e45d142c74d023eccd28c7f9e03b0af2d2b9 100644 GIT binary patch literal 118839 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

0hOT$`donwI~(&Fqvi(oj|qzVR4* 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]V;ZJ?Jx7eS(*KURO{y5^Zh7CMY>g9 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

8fq~0=I!v4AnE;d}cbadUlY^=u7 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 diff --git a/vendor/github.com/rs/zerolog/syslog.go b/vendor/github.com/rs/zerolog/syslog.go index c408283..a2b7285 100644 --- a/vendor/github.com/rs/zerolog/syslog.go +++ b/vendor/github.com/rs/zerolog/syslog.go @@ -78,3 +78,12 @@ func (sw syslogWriter) WriteLevel(level Level, p []byte) (n int, err error) { n = len(p) return } + +// Call the underlying writer's Close method if it is an io.Closer. Otherwise +// does nothing. +func (sw syslogWriter) Close() error { + if c, ok := sw.w.(io.Closer); ok { + return c.Close() + } + return nil +} diff --git a/vendor/github.com/rs/zerolog/writer.go b/vendor/github.com/rs/zerolog/writer.go index 9b9ef88..41b394d 100644 --- a/vendor/github.com/rs/zerolog/writer.go +++ b/vendor/github.com/rs/zerolog/writer.go @@ -27,6 +27,15 @@ func (lw LevelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error) { return lw.Write(p) } +// Call the underlying writer's Close method if it is an io.Closer. Otherwise +// does nothing. +func (lw LevelWriterAdapter) Close() error { + if closer, ok := lw.Writer.(io.Closer); ok { + return closer.Close() + } + return nil +} + type syncWriter struct { mu sync.Mutex lw LevelWriter @@ -57,6 +66,15 @@ func (s *syncWriter) WriteLevel(l Level, p []byte) (n int, err error) { return s.lw.WriteLevel(l, p) } +func (s *syncWriter) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + if closer, ok := s.lw.(io.Closer); ok { + return closer.Close() + } + return nil +} + type multiLevelWriter struct { writers []LevelWriter } @@ -89,6 +107,20 @@ func (t multiLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) { return n, err } +// Calls close on all the underlying writers that are io.Closers. If any of the +// Close methods return an error, the remainder of the closers are not closed +// and the error is returned. +func (t multiLevelWriter) Close() error { + for _, w := range t.writers { + if closer, ok := w.(io.Closer); ok { + if err := closer.Close(); err != nil { + return err + } + } + } + return nil +} + // MultiLevelWriter creates a writer that duplicates its writes to all the // provided writers, similar to the Unix tee(1) command. If some writers // implement LevelWriter, their WriteLevel method will be used instead of Write. @@ -180,3 +212,135 @@ func (w *FilteredLevelWriter) WriteLevel(level Level, p []byte) (int, error) { } return len(p), nil } + +var triggerWriterPool = &sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 0, 1024)) + }, +} + +// TriggerLevelWriter buffers log lines at the ConditionalLevel or below +// until a trigger level (or higher) line is emitted. Log lines with level +// higher than ConditionalLevel are always written out to the destination +// writer. If trigger never happens, buffered log lines are never written out. +// +// It can be used to configure "log level per request". +type TriggerLevelWriter struct { + // Destination writer. If LevelWriter is provided (usually), its WriteLevel is used + // instead of Write. + io.Writer + + // ConditionalLevel is the level (and below) at which lines are buffered until + // a trigger level (or higher) line is emitted. Usually this is set to DebugLevel. + ConditionalLevel Level + + // TriggerLevel is the lowest level that triggers the sending of the conditional + // level lines. Usually this is set to ErrorLevel. + TriggerLevel Level + + buf *bytes.Buffer + triggered bool + mu sync.Mutex +} + +func (w *TriggerLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + // At first trigger level or above log line, we flush the buffer and change the + // trigger state to triggered. + if !w.triggered && l >= w.TriggerLevel { + err := w.trigger() + if err != nil { + return 0, err + } + } + + // Unless triggered, we buffer everything at and below ConditionalLevel. + if !w.triggered && l <= w.ConditionalLevel { + if w.buf == nil { + w.buf = triggerWriterPool.Get().(*bytes.Buffer) + } + + // We prefix each log line with a byte with the level. + // Hopefully we will never have a level value which equals a newline + // (which could interfere with reconstruction of log lines in the trigger method). + w.buf.WriteByte(byte(l)) + w.buf.Write(p) + return len(p), nil + } + + // Anything above ConditionalLevel is always passed through. + // Once triggered, everything is passed through. + if lw, ok := w.Writer.(LevelWriter); ok { + return lw.WriteLevel(l, p) + } + return w.Write(p) +} + +// trigger expects lock to be held. +func (w *TriggerLevelWriter) trigger() error { + if w.triggered { + return nil + } + w.triggered = true + + if w.buf == nil { + return nil + } + + p := w.buf.Bytes() + for len(p) > 0 { + // We do not use bufio.Scanner here because we already have full buffer + // in the memory and we do not want extra copying from the buffer to + // scanner's token slice, nor we want to hit scanner's token size limit, + // and we also want to preserve newlines. + i := bytes.IndexByte(p, '\n') + line := p[0 : i+1] + p = p[i+1:] + // We prefixed each log line with a byte with the level. + level := Level(line[0]) + line = line[1:] + var err error + if lw, ok := w.Writer.(LevelWriter); ok { + _, err = lw.WriteLevel(level, line) + } else { + _, err = w.Write(line) + } + if err != nil { + return err + } + } + + return nil +} + +// Trigger forces flushing the buffer and change the trigger state to +// triggered, if the writer has not already been triggered before. +func (w *TriggerLevelWriter) Trigger() error { + w.mu.Lock() + defer w.mu.Unlock() + + return w.trigger() +} + +// Close closes the writer and returns the buffer to the pool. +func (w *TriggerLevelWriter) Close() error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.buf == nil { + return nil + } + + // We return the buffer only if it has not grown above the limit. + // This prevents accumulation of large buffers in the pool just + // because occasionally a large buffer might be needed. + if w.buf.Cap() <= TriggerLevelWriterBufferReuseLimit { + w.buf.Reset() + triggerWriterPool.Put(w.buf) + } + w.buf = nil + + return nil +} diff --git a/vendor/github.com/yuin/goldmark/README.md b/vendor/github.com/yuin/goldmark/README.md index 8d9d83f..a836445 100644 --- a/vendor/github.com/yuin/goldmark/README.md +++ b/vendor/github.com/yuin/goldmark/README.md @@ -8,7 +8,7 @@ goldmark > A Markdown parser written in Go. Easy to extend, standards-compliant, well-structured. -goldmark is compliant with CommonMark 0.30. +goldmark is compliant with CommonMark 0.31.2. Motivation ---------------------- @@ -260,7 +260,7 @@ You can override autolinking patterns via options. | Functional option | Type | Description | | ----------------- | ---- | ----------- | -| `extension.WithLinkifyAllowedProtocols` | `[][]byte` | List of allowed protocols such as `[][]byte{ []byte("http:") }` | +| `extension.WithLinkifyAllowedProtocols` | `[][]byte \| []string` | List of allowed protocols such as `[]string{ "http:" }` | | `extension.WithLinkifyURLRegexp` | `*regexp.Regexp` | Regexp that defines URLs, including protocols | | `extension.WithLinkifyWWWRegexp` | `*regexp.Regexp` | Regexp that defines URL starting with `www.`. This pattern corresponds to [the extended www autolink](https://github.github.com/gfm/#extended-www-autolink) | | `extension.WithLinkifyEmailRegexp` | `*regexp.Regexp` | Regexp that defines email addresses` | @@ -277,9 +277,9 @@ markdown := goldmark.New( ), goldmark.WithExtensions( extension.NewLinkify( - extension.WithLinkifyAllowedProtocols([][]byte{ - []byte("http:"), - []byte("https:"), + extension.WithLinkifyAllowedProtocols([]string{ + "http:", + "https:", }), extension.WithLinkifyURLRegexp( xurls.Strict, @@ -297,13 +297,13 @@ This extension has some options: | Functional option | Type | Description | | ----------------- | ---- | ----------- | -| `extension.WithFootnoteIDPrefix` | `[]byte` | a prefix for the id attributes.| +| `extension.WithFootnoteIDPrefix` | `[]byte \| string` | a prefix for the id attributes.| | `extension.WithFootnoteIDPrefixFunction` | `func(gast.Node) []byte` | a function that determines the id attribute for given Node.| -| `extension.WithFootnoteLinkTitle` | `[]byte` | an optional title attribute for footnote links.| -| `extension.WithFootnoteBacklinkTitle` | `[]byte` | an optional title attribute for footnote backlinks. | -| `extension.WithFootnoteLinkClass` | `[]byte` | a class for footnote links. This defaults to `footnote-ref`. | -| `extension.WithFootnoteBacklinkClass` | `[]byte` | a class for footnote backlinks. This defaults to `footnote-backref`. | -| `extension.WithFootnoteBacklinkHTML` | `[]byte` | a class for footnote backlinks. This defaults to `↩︎`. | +| `extension.WithFootnoteLinkTitle` | `[]byte \| string` | an optional title attribute for footnote links.| +| `extension.WithFootnoteBacklinkTitle` | `[]byte \| string` | an optional title attribute for footnote backlinks. | +| `extension.WithFootnoteLinkClass` | `[]byte \| string` | a class for footnote links. This defaults to `footnote-ref`. | +| `extension.WithFootnoteBacklinkClass` | `[]byte \| string` | a class for footnote backlinks. This defaults to `footnote-backref`. | +| `extension.WithFootnoteBacklinkHTML` | `[]byte \| string` | a class for footnote backlinks. This defaults to `↩︎`. | Some options can have special substitutions. Occurrences of “^^” in the string will be replaced by the corresponding footnote number in the HTML output. Occurrences of “%%” will be replaced by a number for the reference (footnotes can have multiple references). @@ -319,7 +319,7 @@ for _, path := range files { markdown := goldmark.New( goldmark.WithExtensions( NewFootnote( - WithFootnoteIDPrefix([]byte(path)), + WithFootnoteIDPrefix(path), ), ), ) @@ -379,7 +379,7 @@ This extension provides additional options for CJK users. | Functional option | Type | Description | | ----------------- | ---- | ----------- | -| `extension.WithEastAsianLineBreaks` | `...extension.EastAsianLineBreaksStyle` | Soft line breaks are rendered as a newline. Some asian users will see it as an unnecessary space. With this option, soft line breaks between east asian wide characters will be ignored. | +| `extension.WithEastAsianLineBreaks` | `...extension.EastAsianLineBreaksStyle` | Soft line breaks are rendered as a newline. Some asian users will see it as an unnecessary space. With this option, soft line breaks between east asian wide characters will be ignored. This defaults to `EastAsianLineBreaksStyleSimple`. | | `extension.WithEscapedSpace` | `-` | Without spaces around an emphasis started with east asian punctuations, it is not interpreted as an emphasis(as defined in CommonMark spec). With this option, you can avoid this inconvenient behavior by putting 'not rendered' spaces around an emphasis like `太郎は\ **「こんにちわ」**\ といった`. | #### Styles of Line Breaking @@ -467,6 +467,7 @@ As you can see, goldmark's performance is on par with cmark's. Extensions -------------------- +### List of extensions - [goldmark-meta](https://github.com/yuin/goldmark-meta): A YAML metadata extension for the goldmark Markdown parser. @@ -490,6 +491,13 @@ Extensions - [goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2): Adds support for [D2](https://d2lang.com/) diagrams. - [goldmark-katex](https://github.com/FurqanSoftware/goldmark-katex): Adds support for [KaTeX](https://katex.org/) math and equations. - [goldmark-img64](https://github.com/tenkoh/goldmark-img64): Adds support for embedding images into the document as DataURL (base64 encoded). +- [goldmark-enclave](https://github.com/quail-ink/goldmark-enclave): Adds support for embedding youtube/bilibili video, X's [oembed tweet](https://publish.twitter.com/), [tradingview](https://www.tradingview.com/widget/)'s chart, [quail](https://quail.ink)'s widget into the document. +- [goldmark-wiki-table](https://github.com/movsb/goldmark-wiki-table): Adds support for embedding Wiki Tables. + +### Loading extensions at runtime +[goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) allows you to write a goldmark extension in Lua and load it at runtime without re-compilation. + +Please refer to [goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) for details. goldmark internal(for extension developers) diff --git a/vendor/github.com/yuin/goldmark/extension/footnote.go b/vendor/github.com/yuin/goldmark/extension/footnote.go index d1b67aa..2e22526 100644 --- a/vendor/github.com/yuin/goldmark/extension/footnote.go +++ b/vendor/github.com/yuin/goldmark/extension/footnote.go @@ -382,8 +382,8 @@ func (o *withFootnoteIDPrefix) SetFootnoteOption(c *FootnoteConfig) { } // WithFootnoteIDPrefix is a functional option that is a prefix for the id attributes generated by footnotes. -func WithFootnoteIDPrefix(a []byte) FootnoteOption { - return &withFootnoteIDPrefix{a} +func WithFootnoteIDPrefix[T []byte | string](a T) FootnoteOption { + return &withFootnoteIDPrefix{[]byte(a)} } const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction" @@ -420,8 +420,8 @@ func (o *withFootnoteLinkTitle) SetFootnoteOption(c *FootnoteConfig) { } // WithFootnoteLinkTitle is a functional option that is an optional title attribute for footnote links. -func WithFootnoteLinkTitle(a []byte) FootnoteOption { - return &withFootnoteLinkTitle{a} +func WithFootnoteLinkTitle[T []byte | string](a T) FootnoteOption { + return &withFootnoteLinkTitle{[]byte(a)} } const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle" @@ -439,8 +439,8 @@ func (o *withFootnoteBacklinkTitle) SetFootnoteOption(c *FootnoteConfig) { } // WithFootnoteBacklinkTitle is a functional option that is an optional title attribute for footnote backlinks. -func WithFootnoteBacklinkTitle(a []byte) FootnoteOption { - return &withFootnoteBacklinkTitle{a} +func WithFootnoteBacklinkTitle[T []byte | string](a T) FootnoteOption { + return &withFootnoteBacklinkTitle{[]byte(a)} } const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass" @@ -458,8 +458,8 @@ func (o *withFootnoteLinkClass) SetFootnoteOption(c *FootnoteConfig) { } // WithFootnoteLinkClass is a functional option that is a class for footnote links. -func WithFootnoteLinkClass(a []byte) FootnoteOption { - return &withFootnoteLinkClass{a} +func WithFootnoteLinkClass[T []byte | string](a T) FootnoteOption { + return &withFootnoteLinkClass{[]byte(a)} } const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass" @@ -477,8 +477,8 @@ func (o *withFootnoteBacklinkClass) SetFootnoteOption(c *FootnoteConfig) { } // WithFootnoteBacklinkClass is a functional option that is a class for footnote backlinks. -func WithFootnoteBacklinkClass(a []byte) FootnoteOption { - return &withFootnoteBacklinkClass{a} +func WithFootnoteBacklinkClass[T []byte | string](a T) FootnoteOption { + return &withFootnoteBacklinkClass{[]byte(a)} } const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML" @@ -496,8 +496,8 @@ func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) { } // WithFootnoteBacklinkHTML is an HTML content for footnote backlinks. -func WithFootnoteBacklinkHTML(a []byte) FootnoteOption { - return &withFootnoteBacklinkHTML{a} +func WithFootnoteBacklinkHTML[T []byte | string](a T) FootnoteOption { + return &withFootnoteBacklinkHTML{[]byte(a)} } // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that diff --git a/vendor/github.com/yuin/goldmark/extension/linkify.go b/vendor/github.com/yuin/goldmark/extension/linkify.go index 0f23e90..ad88933 100644 --- a/vendor/github.com/yuin/goldmark/extension/linkify.go +++ b/vendor/github.com/yuin/goldmark/extension/linkify.go @@ -66,10 +66,12 @@ func (o *withLinkifyAllowedProtocols) SetLinkifyOption(p *LinkifyConfig) { // WithLinkifyAllowedProtocols is a functional option that specify allowed // protocols in autolinks. Each protocol must end with ':' like // 'http:' . -func WithLinkifyAllowedProtocols(value [][]byte) LinkifyOption { - return &withLinkifyAllowedProtocols{ - value: value, +func WithLinkifyAllowedProtocols[T []byte | string](value []T) LinkifyOption { + opt := &withLinkifyAllowedProtocols{} + for _, v := range value { + opt.value = append(opt.value, []byte(v)) } + return opt } type withLinkifyURLRegexp struct { diff --git a/vendor/github.com/yuin/goldmark/extension/typographer.go b/vendor/github.com/yuin/goldmark/extension/typographer.go index 259c4f7..44c15eb 100644 --- a/vendor/github.com/yuin/goldmark/extension/typographer.go +++ b/vendor/github.com/yuin/goldmark/extension/typographer.go @@ -115,10 +115,10 @@ func (o *withTypographicSubstitutions) SetTypographerOption(p *TypographerConfig // WithTypographicSubstitutions is a functional otpion that specify replacement text // for punctuations. -func WithTypographicSubstitutions(values map[TypographicPunctuation][]byte) TypographerOption { +func WithTypographicSubstitutions[T []byte | string](values map[TypographicPunctuation]T) TypographerOption { replacements := newDefaultSubstitutions() for k, v := range values { - replacements[k] = v + replacements[k] = []byte(v) } return &withTypographicSubstitutions{replacements} diff --git a/vendor/github.com/yuin/goldmark/parser/html_block.go b/vendor/github.com/yuin/goldmark/parser/html_block.go index bf0258b..09dc21f 100644 --- a/vendor/github.com/yuin/goldmark/parser/html_block.go +++ b/vendor/github.com/yuin/goldmark/parser/html_block.go @@ -61,8 +61,8 @@ var allowedBlockTags = map[string]bool{ "option": true, "p": true, "param": true, + "search": true, "section": true, - "source": true, "summary": true, "table": true, "tbody": true, diff --git a/vendor/github.com/yuin/goldmark/parser/raw_html.go b/vendor/github.com/yuin/goldmark/parser/raw_html.go index 2b3dbc2..1d582a7 100644 --- a/vendor/github.com/yuin/goldmark/parser/raw_html.go +++ b/vendor/github.com/yuin/goldmark/parser/raw_html.go @@ -58,47 +58,38 @@ var closeProcessingInstruction = []byte("?>") var openCDATA = []byte("") var closeDecl = []byte(">") -var emptyComment = []byte("") -var invalidComment1 = []byte("") -var invalidComment2 = []byte("") +var emptyComment1 = []byte("") +var emptyComment2 = []byte("") var openComment = []byte("") -var doubleHyphen = []byte("--") func (s *rawHTMLParser) parseComment(block text.Reader, pc Context) ast.Node { savedLine, savedSegment := block.Position() node := ast.NewRawHTML() line, segment := block.PeekLine() - if bytes.HasPrefix(line, emptyComment) { - node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment))) - block.Advance(len(emptyComment)) + if bytes.HasPrefix(line, emptyComment1) { + node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment1))) + block.Advance(len(emptyComment1)) return node } - if bytes.HasPrefix(line, invalidComment1) || bytes.HasPrefix(line, invalidComment2) { - return nil + if bytes.HasPrefix(line, emptyComment2) { + node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment2))) + block.Advance(len(emptyComment2)) + return node } offset := len(openComment) line = line[offset:] for { - hindex := bytes.Index(line, doubleHyphen) - if hindex > -1 { - hindex += offset - } - index := bytes.Index(line, closeComment) + offset - if index > -1 && hindex == index { - if index == 0 || len(line) < 2 || line[index-offset-1] != '-' { - node.Segments.Append(segment.WithStop(segment.Start + index + len(closeComment))) - block.Advance(index + len(closeComment)) - return node - } - } - if hindex > 0 { - break + index := bytes.Index(line, closeComment) + if index > -1 { + node.Segments.Append(segment.WithStop(segment.Start + offset + index + len(closeComment))) + block.Advance(offset + index + len(closeComment)) + return node } + offset = 0 node.Segments.Append(segment) block.AdvanceLine() line, segment = block.PeekLine() - offset = 0 if line == nil { break } diff --git a/vendor/github.com/yuin/goldmark/util/util.go b/vendor/github.com/yuin/goldmark/util/util.go index 9bf09ad..e2c92c6 100644 --- a/vendor/github.com/yuin/goldmark/util/util.go +++ b/vendor/github.com/yuin/goldmark/util/util.go @@ -808,7 +808,7 @@ func IsPunct(c byte) bool { // IsPunctRune returns true if the given rune is a punctuation, otherwise false. func IsPunctRune(r rune) bool { - return int32(r) <= 256 && IsPunct(byte(r)) || unicode.IsPunct(r) + return unicode.IsSymbol(r) || unicode.IsPunct(r) } // IsSpace returns true if the given character is a space, otherwise false. diff --git a/vendor/gitlab.com/etke.cc/go/env/LICENSE b/vendor/gitlab.com/etke.cc/go/env/LICENSE index f6a320f..5357f69 100644 --- a/vendor/gitlab.com/etke.cc/go/env/LICENSE +++ b/vendor/gitlab.com/etke.cc/go/env/LICENSE @@ -1,661 +1,166 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - env - Copyright (C) 2022 etke.cc / Go - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/vendor/gitlab.com/etke.cc/go/env/env.go b/vendor/gitlab.com/etke.cc/go/env/env.go index 2770bfd..1795d2f 100644 --- a/vendor/gitlab.com/etke.cc/go/env/env.go +++ b/vendor/gitlab.com/etke.cc/go/env/env.go @@ -14,26 +14,36 @@ func SetPrefix(prefix string) { } // String returns string vars -func String(shortkey string, defaultValue string) string { +func String(shortkey string, defaultValue ...string) string { + var dv string + if len(defaultValue) > 0 { + dv = defaultValue[0] + } + key := strings.ToUpper(envprefix + "_" + strings.ReplaceAll(shortkey, ".", "_")) value := strings.TrimSpace(os.Getenv(key)) if value == "" { - return defaultValue + return dv } return value } // Int returns int vars -func Int(shortkey string, defaultValue int) int { - str := String(shortkey, "") +func Int(shortkey string, defaultValue ...int) int { + var dv int + if len(defaultValue) > 0 { + dv = defaultValue[0] + } + + str := String(shortkey) if str == "" { - return defaultValue + return dv } val, err := strconv.Atoi(str) if err != nil { - return defaultValue + return dv } return val @@ -41,7 +51,7 @@ func Int(shortkey string, defaultValue int) int { // Bool returns boolean vars (1, true, yes) func Bool(shortkey string) bool { - str := strings.ToLower(String(shortkey, "")) + str := strings.ToLower(String(shortkey)) if str == "" { return false } @@ -50,7 +60,7 @@ func Bool(shortkey string) bool { // Slice returns slice from space-separated strings, eg: export VAR="one two three" => []string{"one", "two", "three"} func Slice(shortkey string) []string { - str := String(shortkey, "") + str := String(shortkey) if str == "" { return nil } diff --git a/vendor/gitlab.com/etke.cc/linkpearl/accountdata.go b/vendor/gitlab.com/etke.cc/linkpearl/accountdata.go index c438c47..36052af 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/accountdata.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/accountdata.go @@ -1,13 +1,14 @@ package linkpearl import ( + "context" "strings" "maunium.net/go/mautrix/id" ) // GetAccountData of the user (from cache and API, with encryption support) -func (l *Linkpearl) GetAccountData(name string) (map[string]string, error) { +func (l *Linkpearl) GetAccountData(ctx context.Context, name string) (map[string]string, error) { cached, ok := l.acc.Get(name) if ok { if cached == nil { @@ -17,7 +18,7 @@ func (l *Linkpearl) GetAccountData(name string) (map[string]string, error) { } var data map[string]string - err := l.GetClient().GetAccountData(name, &data) + err := l.GetClient().GetAccountData(ctx, name, &data) if err != nil { data = map[string]string{} if strings.Contains(err.Error(), "M_NOT_FOUND") { @@ -33,15 +34,15 @@ func (l *Linkpearl) GetAccountData(name string) (map[string]string, error) { } // SetAccountData of the user (to cache and API, with encryption support) -func (l *Linkpearl) SetAccountData(name string, data map[string]string) error { +func (l *Linkpearl) SetAccountData(ctx context.Context, name string, data map[string]string) error { l.acc.Add(name, data) data = l.encryptAccountData(data) - return UnwrapError(l.GetClient().SetAccountData(name, data)) + return UnwrapError(l.GetClient().SetAccountData(ctx, name, data)) } // GetRoomAccountData of the room (from cache and API, with encryption support) -func (l *Linkpearl) GetRoomAccountData(roomID id.RoomID, name string) (map[string]string, error) { +func (l *Linkpearl) GetRoomAccountData(ctx context.Context, roomID id.RoomID, name string) (map[string]string, error) { key := roomID.String() + name cached, ok := l.acc.Get(key) if ok { @@ -52,7 +53,7 @@ func (l *Linkpearl) GetRoomAccountData(roomID id.RoomID, name string) (map[strin } var data map[string]string - err := l.GetClient().GetRoomAccountData(roomID, name, &data) + err := l.GetClient().GetRoomAccountData(ctx, roomID, name, &data) if err != nil { data = map[string]string{} if strings.Contains(err.Error(), "M_NOT_FOUND") { @@ -68,12 +69,12 @@ func (l *Linkpearl) GetRoomAccountData(roomID id.RoomID, name string) (map[strin } // SetRoomAccountData of the room (to cache and API, with encryption support) -func (l *Linkpearl) SetRoomAccountData(roomID id.RoomID, name string, data map[string]string) error { +func (l *Linkpearl) SetRoomAccountData(ctx context.Context, roomID id.RoomID, name string, data map[string]string) error { key := roomID.String() + name l.acc.Add(key, data) data = l.encryptAccountData(data) - return UnwrapError(l.GetClient().SetRoomAccountData(roomID, name, data)) + return UnwrapError(l.GetClient().SetRoomAccountData(ctx, roomID, name, data)) } func (l *Linkpearl) encryptAccountData(data map[string]string) map[string]string { diff --git a/vendor/gitlab.com/etke.cc/linkpearl/config.go b/vendor/gitlab.com/etke.cc/linkpearl/config.go index f132c84..6cb09a5 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/config.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/config.go @@ -1,6 +1,7 @@ package linkpearl import ( + "context" "crypto/hmac" "crypto/sha512" "database/sql" @@ -25,7 +26,7 @@ type Config struct { // JoinPermit is a callback function that tells // if linkpearl should respond to the given "invite" event // and join the room - JoinPermit func(*event.Event) bool + JoinPermit func(context.Context, *event.Event) bool // AutoLeave if true, linkpearl will automatically leave empty rooms AutoLeave bool diff --git a/vendor/gitlab.com/etke.cc/linkpearl/events.go b/vendor/gitlab.com/etke.cc/linkpearl/events.go index 5861a00..e9531c1 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/events.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/events.go @@ -1,6 +1,7 @@ package linkpearl import ( + "context" "strconv" "maunium.net/go/mautrix" @@ -15,7 +16,7 @@ type RespThreads struct { } // Threads endpoint, ref: https://spec.matrix.org/v1.8/client-server-api/#get_matrixclientv1roomsroomidthreads -func (l *Linkpearl) Threads(roomID id.RoomID, fromToken ...string) (*RespThreads, error) { +func (l *Linkpearl) Threads(ctx context.Context, roomID id.RoomID, fromToken ...string) (*RespThreads, error) { var from string if len(fromToken) > 0 { from = fromToken[0] @@ -28,18 +29,18 @@ func (l *Linkpearl) Threads(roomID id.RoomID, fromToken ...string) (*RespThreads var resp *RespThreads urlPath := l.GetClient().BuildURLWithQuery(mautrix.ClientURLPath{"v1", "rooms", roomID, "threads"}, query) - _, err := l.GetClient().MakeRequest("GET", urlPath, nil, &resp) + _, err := l.GetClient().MakeRequest(ctx, "GET", urlPath, nil, &resp) return resp, UnwrapError(err) } // FindThreadBy tries to find thread message event by field and value -func (l *Linkpearl) FindThreadBy(roomID id.RoomID, field, value string, fromToken ...string) *event.Event { +func (l *Linkpearl) FindThreadBy(ctx context.Context, roomID id.RoomID, field, value string, fromToken ...string) *event.Event { var from string if len(fromToken) > 0 { from = fromToken[0] } - resp, err := l.Threads(roomID, from) + resp, err := l.Threads(ctx, roomID, from) err = UnwrapError(err) if err != nil { l.log.Warn().Err(err).Str("roomID", roomID.String()).Msg("cannot get room threads") @@ -47,7 +48,7 @@ func (l *Linkpearl) FindThreadBy(roomID id.RoomID, field, value string, fromToke } for _, msg := range resp.Chunk { - evt, contains := l.eventContains(msg, field, value) + evt, contains := l.eventContains(ctx, msg, field, value) if contains { return evt } @@ -57,17 +58,17 @@ func (l *Linkpearl) FindThreadBy(roomID id.RoomID, field, value string, fromToke return nil } - return l.FindThreadBy(roomID, field, value, resp.NextBatch) + return l.FindThreadBy(ctx, roomID, field, value, resp.NextBatch) } // FindEventBy tries to find message event by field and value -func (l *Linkpearl) FindEventBy(roomID id.RoomID, field, value string, fromToken ...string) *event.Event { +func (l *Linkpearl) FindEventBy(ctx context.Context, roomID id.RoomID, field, value string, fromToken ...string) *event.Event { var from string if len(fromToken) > 0 { from = fromToken[0] } - resp, err := l.GetClient().Messages(roomID, from, "", mautrix.DirectionBackward, nil, l.eventsLimit) + resp, err := l.GetClient().Messages(ctx, roomID, from, "", mautrix.DirectionBackward, nil, l.eventsLimit) err = UnwrapError(err) if err != nil { l.log.Warn().Err(err).Str("roomID", roomID.String()).Msg("cannot get room events") @@ -75,7 +76,7 @@ func (l *Linkpearl) FindEventBy(roomID id.RoomID, field, value string, fromToken } for _, msg := range resp.Chunk { - evt, contains := l.eventContains(msg, field, value) + evt, contains := l.eventContains(ctx, msg, field, value) if contains { return evt } @@ -85,13 +86,13 @@ func (l *Linkpearl) FindEventBy(roomID id.RoomID, field, value string, fromToken return nil } - return l.FindEventBy(roomID, field, value, resp.End) + return l.FindEventBy(ctx, roomID, field, value, resp.End) } -func (l *Linkpearl) eventContains(evt *event.Event, field, value string) (*event.Event, bool) { +func (l *Linkpearl) eventContains(ctx context.Context, evt *event.Event, field, value string) (*event.Event, bool) { if evt.Type == event.EventEncrypted { ParseContent(evt, &l.log) - decrypted, err := l.GetClient().Crypto.Decrypt(evt) + decrypted, err := l.GetClient().Crypto.Decrypt(ctx, evt) if err == nil { evt = decrypted } diff --git a/vendor/gitlab.com/etke.cc/linkpearl/linkpearl.go b/vendor/gitlab.com/etke.cc/linkpearl/linkpearl.go index 2830a6f..db7886f 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/linkpearl.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/linkpearl.go @@ -2,6 +2,7 @@ package linkpearl import ( + "context" "database/sql" lru "github.com/hashicorp/golang-lru/v2" @@ -31,7 +32,7 @@ type Linkpearl struct { log zerolog.Logger api *mautrix.Client - joinPermit func(*event.Event) bool + joinPermit func(ctx context.Context, evt *event.Event) bool autoleave bool maxretries int eventsLimit int @@ -54,7 +55,7 @@ func setDefaults(cfg *Config) { } if cfg.JoinPermit == nil { // By default, we approve all join requests - cfg.JoinPermit = func(*event.Event) bool { return true } + cfg.JoinPermit = func(_ context.Context, _ *event.Event) bool { return true } } } @@ -103,7 +104,7 @@ func New(cfg *Config) (*Linkpearl, error) { return nil, err } lp.ch.LoginAs = cfg.LoginAs() - if err = lp.ch.Init(); err != nil { + if err = lp.ch.Init(context.Background()); err != nil { return nil, err } lp.api.Crypto = lp.ch @@ -131,16 +132,16 @@ func (l *Linkpearl) GetAccountDataCrypter() *Crypter { } // SetPresence (own). See https://spec.matrix.org/v1.3/client-server-api/#put_matrixclientv3presenceuseridstatus -func (l *Linkpearl) SetPresence(presence event.Presence, message string) error { +func (l *Linkpearl) SetPresence(ctx context.Context, presence event.Presence, message string) error { req := ReqPresence{Presence: presence, StatusMsg: message} u := l.GetClient().BuildClientURL("v3", "presence", l.GetClient().UserID, "status") - _, err := l.GetClient().MakeRequest("PUT", u, req, nil) + _, err := l.GetClient().MakeRequest(ctx, "PUT", u, req, nil) return err } // SetJoinPermit sets the the join permit callback function -func (l *Linkpearl) SetJoinPermit(value func(*event.Event) bool) { +func (l *Linkpearl) SetJoinPermit(value func(context.Context, *event.Event) bool) { l.joinPermit = value } @@ -152,7 +153,7 @@ func (l *Linkpearl) Start(optionalStatusMsg ...string) error { statusMsg = optionalStatusMsg[0] } - err := l.SetPresence(event.PresenceOnline, statusMsg) + err := l.SetPresence(context.Background(), event.PresenceOnline, statusMsg) if err != nil { l.log.Error().Err(err).Msg("cannot set presence") } @@ -165,7 +166,7 @@ func (l *Linkpearl) Start(optionalStatusMsg ...string) error { // Stop the client func (l *Linkpearl) Stop() { l.log.Debug().Msg("stopping the client") - if err := l.api.SetPresence(event.PresenceOffline); err != nil { + if err := l.api.SetPresence(context.Background(), event.PresenceOffline); err != nil { l.log.Error().Err(err).Msg("cannot set presence") } l.api.StopSync() diff --git a/vendor/gitlab.com/etke.cc/linkpearl/send.go b/vendor/gitlab.com/etke.cc/linkpearl/send.go index dbb04d7..82f687a 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/send.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/send.go @@ -1,6 +1,8 @@ package linkpearl import ( + "context" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" @@ -10,9 +12,9 @@ import ( // Send a message to the roomID and automatically try to encrypt it, if the destination room is encrypted // //nolint:unparam // it's public interface -func (l *Linkpearl) Send(roomID id.RoomID, content interface{}) (id.EventID, error) { +func (l *Linkpearl) Send(ctx context.Context, roomID id.RoomID, content interface{}) (id.EventID, error) { l.log.Debug().Str("roomID", roomID.String()).Any("content", content).Msg("sending event") - resp, err := l.api.SendMessageEvent(roomID, event.EventMessage, content) + resp, err := l.api.SendMessageEvent(ctx, roomID, event.EventMessage, content) if err != nil { return "", UnwrapError(err) } @@ -20,7 +22,7 @@ func (l *Linkpearl) Send(roomID id.RoomID, content interface{}) (id.EventID, err } // SendNotice to a room with optional relations, markdown supported -func (l *Linkpearl) SendNotice(roomID id.RoomID, message string, relates ...*event.RelatesTo) { +func (l *Linkpearl) SendNotice(ctx context.Context, roomID id.RoomID, message string, relates ...*event.RelatesTo) { var withRelatesTo bool content := format.RenderMarkdown(message, true, true) content.MsgType = event.MsgNotice @@ -29,12 +31,12 @@ func (l *Linkpearl) SendNotice(roomID id.RoomID, message string, relates ...*eve content.RelatesTo = relates[0] } - _, err := l.Send(roomID, &content) + _, err := l.Send(ctx, roomID, &content) if err != nil { l.log.Error().Err(UnwrapError(err)).Str("roomID", roomID.String()).Str("retries", "1/2").Msg("cannot send a notice into the room") if withRelatesTo { content.RelatesTo = nil - _, err = l.Send(roomID, &content) + _, err = l.Send(ctx, roomID, &content) if err != nil { l.log.Error().Err(UnwrapError(err)).Str("roomID", roomID.String()).Str("retries", "2/2").Msg("cannot send a notice into the room even without relations") } @@ -43,13 +45,13 @@ func (l *Linkpearl) SendNotice(roomID id.RoomID, message string, relates ...*eve } // SendFile to a matrix room -func (l *Linkpearl) SendFile(roomID id.RoomID, req *mautrix.ReqUploadMedia, msgtype event.MessageType, relates ...*event.RelatesTo) error { +func (l *Linkpearl) SendFile(ctx context.Context, roomID id.RoomID, req *mautrix.ReqUploadMedia, msgtype event.MessageType, relates ...*event.RelatesTo) error { var relation *event.RelatesTo if len(relates) > 0 { relation = relates[0] } - resp, err := l.GetClient().UploadMedia(*req) + resp, err := l.GetClient().UploadMedia(ctx, *req) if err != nil { err = UnwrapError(err) l.log.Error().Err(err).Str("file", req.FileName).Msg("cannot upload file") @@ -62,13 +64,13 @@ func (l *Linkpearl) SendFile(roomID id.RoomID, req *mautrix.ReqUploadMedia, msgt RelatesTo: relation, } - _, err = l.Send(roomID, content) + _, err = l.Send(ctx, roomID, content) err = UnwrapError(err) if err != nil { l.log.Error().Err(err).Str("roomID", roomID.String()).Str("retries", "1/2").Msg("cannot send file into the room") if relation != nil { content.RelatesTo = nil - _, err = l.Send(roomID, &content) + _, err = l.Send(ctx, roomID, &content) err = UnwrapError(err) if err != nil { l.log.Error().Err(UnwrapError(err)).Str("roomID", roomID.String()).Str("retries", "2/2").Msg("cannot send file into the room even without relations") diff --git a/vendor/gitlab.com/etke.cc/linkpearl/sync.go b/vendor/gitlab.com/etke.cc/linkpearl/sync.go index 3f9fd58..88c3d22 100644 --- a/vendor/gitlab.com/etke.cc/linkpearl/sync.go +++ b/vendor/gitlab.com/etke.cc/linkpearl/sync.go @@ -1,6 +1,7 @@ package linkpearl import ( + "context" "strings" "time" @@ -28,54 +29,56 @@ func (l *Linkpearl) OnEvent(callback mautrix.EventHandler) { func (l *Linkpearl) initSync() { l.api.Syncer.(mautrix.ExtensibleSyncer).OnEventType( event.StateEncryption, - func(source mautrix.EventSource, evt *event.Event) { - go l.onEncryption(source, evt) + func(ctx context.Context, evt *event.Event) { + go l.onEncryption(ctx, evt) }, ) l.api.Syncer.(mautrix.ExtensibleSyncer).OnEventType( event.StateMember, - func(source mautrix.EventSource, evt *event.Event) { - go l.onMembership(source, evt) + func(ctx context.Context, evt *event.Event) { + go l.onMembership(ctx, evt) }, ) } -func (l *Linkpearl) onMembership(src mautrix.EventSource, evt *event.Event) { - l.ch.Machine().HandleMemberEvent(src, evt) - l.api.StateStore.SetMembership(evt.RoomID, id.UserID(evt.GetStateKey()), evt.Content.AsMember().Membership) +func (l *Linkpearl) onMembership(ctx context.Context, evt *event.Event) { + l.ch.Machine().HandleMemberEvent(ctx, evt) + if err := l.api.StateStore.SetMembership(ctx, evt.RoomID, id.UserID(evt.GetStateKey()), evt.Content.AsMember().Membership); err != nil { + l.log.Error().Err(err).Str("roomID", evt.RoomID.String()).Str("userID", evt.GetStateKey()).Msg("cannot set membership") + } // potentially autoaccept invites - l.onInvite(evt) + l.onInvite(ctx, evt) // autoleave empty rooms - l.onEmpty(evt) + l.onEmpty(ctx, evt) } -func (l *Linkpearl) onInvite(evt *event.Event) { +func (l *Linkpearl) onInvite(ctx context.Context, evt *event.Event) { userID := l.api.UserID.String() invite := evt.Content.AsMember().Membership == event.MembershipInvite if !invite || evt.GetStateKey() != userID { return } - if l.joinPermit(evt) { - l.tryJoin(evt.RoomID, 0) + if l.joinPermit(ctx, evt) { + l.tryJoin(ctx, evt.RoomID, 0) return } - l.tryLeave(evt.RoomID, 0) + l.tryLeave(ctx, evt.RoomID, 0) } // TODO: https://spec.matrix.org/v1.8/client-server-api/#post_matrixclientv3joinroomidoralias // endpoint supports server_name param and tells "The servers to attempt to join the room through. One of the servers must be participating in the room.", // meaning you can specify more than 1 server. It is not clear, what format should be used "example.com,example.org", or "example.com example.org", or whatever else. // Moreover, it is not clear if the following values can be used together with that field: l.api.UserID.Homeserver() and evt.Sender.Homeserver() -func (l *Linkpearl) tryJoin(roomID id.RoomID, retry int) { +func (l *Linkpearl) tryJoin(ctx context.Context, roomID id.RoomID, retry int) { if retry >= l.maxretries { return } - _, err := l.api.JoinRoom(roomID.String(), "", nil) + _, err := l.api.JoinRoom(ctx, roomID.String(), "", nil) err = UnwrapError(err) if err != nil { l.log.Error().Err(err).Str("roomID", roomID.String()).Msg("cannot join room") @@ -84,31 +87,31 @@ func (l *Linkpearl) tryJoin(roomID id.RoomID, retry int) { } time.Sleep(5 * time.Second) l.log.Error().Err(err).Str("roomID", roomID.String()).Int("retry", retry+1).Msg("trying to join again") - l.tryJoin(roomID, retry+1) + l.tryJoin(ctx, roomID, retry+1) } } -func (l *Linkpearl) tryLeave(roomID id.RoomID, retry int) { +func (l *Linkpearl) tryLeave(ctx context.Context, roomID id.RoomID, retry int) { if retry >= l.maxretries { return } - _, err := l.api.LeaveRoom(roomID) + _, err := l.api.LeaveRoom(ctx, roomID) err = UnwrapError(err) if err != nil { l.log.Error().Err(err).Str("roomID", roomID.String()).Msg("cannot leave room") time.Sleep(5 * time.Second) l.log.Error().Err(err).Str("roomID", roomID.String()).Int("retry", retry+1).Msg("trying to leave again") - l.tryLeave(roomID, retry+1) + l.tryLeave(ctx, roomID, retry+1) } } -func (l *Linkpearl) onEmpty(evt *event.Event) { +func (l *Linkpearl) onEmpty(ctx context.Context, evt *event.Event) { if !l.autoleave { return } - members, err := l.api.StateStore.GetRoomJoinedOrInvitedMembers(evt.RoomID) + members, err := l.api.StateStore.GetRoomJoinedOrInvitedMembers(ctx, evt.RoomID) err = UnwrapError(err) if err != nil { l.log.Error().Err(err).Str("roomID", evt.RoomID.String()).Msg("cannot get joined or invited members") @@ -119,9 +122,11 @@ func (l *Linkpearl) onEmpty(evt *event.Event) { return } - l.tryLeave(evt.RoomID, 0) + l.tryLeave(ctx, evt.RoomID, 0) } -func (l *Linkpearl) onEncryption(_ mautrix.EventSource, evt *event.Event) { - l.api.StateStore.SetEncryptionEvent(evt.RoomID, evt.Content.AsEncryption()) +func (l *Linkpearl) onEncryption(ctx context.Context, evt *event.Event) { + if err := l.api.StateStore.SetEncryptionEvent(ctx, evt.RoomID, evt.Content.AsEncryption()); err != nil { + l.log.Error().Err(err).Str("roomID", evt.RoomID.String()).Msg("cannot set encryption event") + } } diff --git a/vendor/go.mau.fi/util/dbutil/connlog.go b/vendor/go.mau.fi/util/dbutil/connlog.go index 03c30e9..98ab89f 100644 --- a/vendor/go.mau.fi/util/dbutil/connlog.go +++ b/vendor/go.mau.fi/util/dbutil/connlog.go @@ -9,6 +9,9 @@ package dbutil import ( "context" "database/sql" + "errors" + "strconv" + "strings" "time" ) @@ -19,18 +22,61 @@ type LoggingExecable struct { db *Database } -func (le *LoggingExecable) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { +type pqError interface { + Get(k byte) string +} + +type PQErrorWithLine struct { + Underlying error + Line string +} + +func (pqe *PQErrorWithLine) Error() string { + return pqe.Underlying.Error() +} + +func (pqe *PQErrorWithLine) Unwrap() error { + return pqe.Underlying +} + +func addErrorLine(query string, err error) error { + if err == nil { + return err + } + var pqe pqError + if !errors.As(err, &pqe) { + return err + } + pos, _ := strconv.Atoi(pqe.Get('P')) + pos-- + if pos <= 0 { + return err + } + lines := strings.Split(query, "\n") + for _, line := range lines { + lineRunes := []rune(line) + if pos < len(lineRunes)+1 { + return &PQErrorWithLine{Underlying: err, Line: line} + } + pos -= len(lineRunes) + 1 + } + return err +} + +func (le *LoggingExecable) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { start := time.Now() query = le.db.mutateQuery(query) res, err := le.UnderlyingExecable.ExecContext(ctx, query, args...) + err = addErrorLine(query, err) le.db.Log.QueryTiming(ctx, "Exec", query, args, -1, time.Since(start), err) return res, err } -func (le *LoggingExecable) QueryContext(ctx context.Context, query string, args ...interface{}) (Rows, error) { +func (le *LoggingExecable) QueryContext(ctx context.Context, query string, args ...any) (Rows, error) { start := time.Now() query = le.db.mutateQuery(query) rows, err := le.UnderlyingExecable.QueryContext(ctx, query, args...) + err = addErrorLine(query, err) le.db.Log.QueryTiming(ctx, "Query", query, args, -1, time.Since(start), err) return &LoggingRows{ ctx: ctx, @@ -42,7 +88,7 @@ func (le *LoggingExecable) QueryContext(ctx context.Context, query string, args }, err } -func (le *LoggingExecable) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { +func (le *LoggingExecable) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row { start := time.Now() query = le.db.mutateQuery(query) row := le.UnderlyingExecable.QueryRowContext(ctx, query, args...) @@ -50,18 +96,6 @@ func (le *LoggingExecable) QueryRowContext(ctx context.Context, query string, ar return row } -func (le *LoggingExecable) Exec(query string, args ...interface{}) (sql.Result, error) { - return le.ExecContext(context.Background(), query, args...) -} - -func (le *LoggingExecable) Query(query string, args ...interface{}) (Rows, error) { - return le.QueryContext(context.Background(), query, args...) -} - -func (le *LoggingExecable) QueryRow(query string, args ...interface{}) *sql.Row { - return le.QueryRowContext(context.Background(), query, args...) -} - // loggingDB is a wrapper for LoggingExecable that allows access to BeginTx. // // While LoggingExecable has a pointer to the database and could use BeginTx, it's not technically safe since @@ -89,10 +123,6 @@ func (ld *loggingDB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Logging }, nil } -func (ld *loggingDB) Begin() (*LoggingTxn, error) { - return ld.BeginTx(context.Background(), nil) -} - type LoggingTxn struct { LoggingExecable UnderlyingTx *sql.Tx @@ -129,7 +159,7 @@ type LoggingRows struct { ctx context.Context db *Database query string - args []interface{} + args []any rs Rows start time.Time nrows int diff --git a/vendor/go.mau.fi/util/dbutil/database.go b/vendor/go.mau.fi/util/dbutil/database.go index c4b77a5..0829057 100644 --- a/vendor/go.mau.fi/util/dbutil/database.go +++ b/vendor/go.mau.fi/util/dbutil/database.go @@ -58,7 +58,7 @@ type Rows interface { } type Scannable interface { - Scan(...interface{}) error + Scan(...any) error } // Expected implementations of Scannable @@ -67,30 +67,16 @@ var ( _ Scannable = (Rows)(nil) ) -type UnderlyingContextExecable interface { - ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) - QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) - QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row -} - -type ContextExecable interface { - ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) - QueryContext(ctx context.Context, query string, args ...interface{}) (Rows, error) - QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row -} - type UnderlyingExecable interface { - UnderlyingContextExecable - Exec(query string, args ...interface{}) (sql.Result, error) - Query(query string, args ...interface{}) (*sql.Rows, error) - QueryRow(query string, args ...interface{}) *sql.Row + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } type Execable interface { - ContextExecable - Exec(query string, args ...interface{}) (sql.Result, error) - Query(query string, args ...interface{}) (Rows, error) - QueryRow(query string, args ...interface{}) *sql.Row + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...any) (Rows, error) + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } type Transaction interface { @@ -101,15 +87,15 @@ type Transaction interface { // Expected implementations of Execable var ( - _ UnderlyingExecable = (*sql.Tx)(nil) - _ UnderlyingExecable = (*sql.DB)(nil) - _ Execable = (*LoggingExecable)(nil) - _ Transaction = (*LoggingTxn)(nil) - _ UnderlyingContextExecable = (*sql.Conn)(nil) + _ UnderlyingExecable = (*sql.Tx)(nil) + _ UnderlyingExecable = (*sql.DB)(nil) + _ UnderlyingExecable = (*sql.Conn)(nil) + _ Execable = (*LoggingExecable)(nil) + _ Transaction = (*LoggingTxn)(nil) ) type Database struct { - loggingDB + LoggingDB loggingDB RawDB *sql.DB ReadOnlyDB *sql.DB Owner string @@ -139,7 +125,7 @@ func (db *Database) Child(versionTable string, upgradeTable UpgradeTable, log Da } return &Database{ RawDB: db.RawDB, - loggingDB: db.loggingDB, + LoggingDB: db.LoggingDB, Owner: "", VersionTable: versionTable, UpgradeTable: upgradeTable, @@ -164,8 +150,8 @@ func NewWithDB(db *sql.DB, rawDialect string) (*Database, error) { IgnoreForeignTables: true, VersionTable: "version", } - wrappedDB.loggingDB.UnderlyingExecable = db - wrappedDB.loggingDB.db = wrappedDB + wrappedDB.LoggingDB.UnderlyingExecable = db + wrappedDB.LoggingDB.db = wrappedDB return wrappedDB, nil } @@ -259,7 +245,7 @@ func NewFromConfig(owner string, cfg Config, logger DatabaseLogger) (*Database, if roUri == "" { uriParts := strings.Split(cfg.URI, "?") - var qs url.Values + qs := url.Values{} if len(uriParts) == 2 { var err error qs, err = url.ParseQuery(uriParts[1]) diff --git a/vendor/go.mau.fi/util/dbutil/iter.go b/vendor/go.mau.fi/util/dbutil/iter.go new file mode 100644 index 0000000..4953bcb --- /dev/null +++ b/vendor/go.mau.fi/util/dbutil/iter.go @@ -0,0 +1,72 @@ +// Copyright (c) 2023 Sumner Evans +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package dbutil + +// RowIter is a wrapper for [Rows] that allows conveniently iterating over rows +// with a predefined scanner function. +type RowIter[T any] interface { + // Iter iterates over the rows and calls the given function for each row. + // + // If the function returns false, the iteration is stopped. + // If the function returns an error, the iteration is stopped and the error is + // returned. + Iter(func(T) (bool, error)) error + + // AsList collects all rows into a slice. + AsList() ([]T, error) +} + +type rowIterImpl[T any] struct { + Rows + ConvertRow func(Scannable) (T, error) +} + +// NewRowIter creates a new RowIter from the given Rows and scanner function. +func NewRowIter[T any](rows Rows, convertFn func(Scannable) (T, error)) RowIter[T] { + return &rowIterImpl[T]{Rows: rows, ConvertRow: convertFn} +} + +func ScanSingleColumn[T any](rows Scannable) (val T, err error) { + err = rows.Scan(&val) + return +} + +type NewableDataStruct[T any] interface { + DataStruct[T] + New() T +} + +func ScanDataStruct[T NewableDataStruct[T]](rows Scannable) (T, error) { + var val T + return val.New().Scan(rows) +} + +func (i *rowIterImpl[T]) Iter(fn func(T) (bool, error)) error { + if i == nil || i.Rows == nil { + return nil + } + defer i.Rows.Close() + + for i.Rows.Next() { + if item, err := i.ConvertRow(i.Rows); err != nil { + return err + } else if cont, err := fn(item); err != nil { + return err + } else if !cont { + break + } + } + return i.Rows.Err() +} + +func (i *rowIterImpl[T]) AsList() (list []T, err error) { + err = i.Iter(func(item T) (bool, error) { + list = append(list, item) + return true, nil + }) + return +} diff --git a/vendor/go.mau.fi/util/dbutil/log.go b/vendor/go.mau.fi/util/dbutil/log.go index bd49326..080cab4 100644 --- a/vendor/go.mau.fi/util/dbutil/log.go +++ b/vendor/go.mau.fi/util/dbutil/log.go @@ -10,12 +10,12 @@ import ( ) type DatabaseLogger interface { - QueryTiming(ctx context.Context, method, query string, args []interface{}, nrows int, duration time.Duration, err error) + QueryTiming(ctx context.Context, method, query string, args []any, nrows int, duration time.Duration, err error) WarnUnsupportedVersion(current, compat, latest int) PrepareUpgrade(current, compat, latest int) DoUpgrade(from, to int, message string, txn bool) // Deprecated: legacy warning method, return errors instead - Warn(msg string, args ...interface{}) + Warn(msg string, args ...any) } type noopLogger struct{} @@ -25,9 +25,9 @@ var NoopLogger DatabaseLogger = &noopLogger{} func (n noopLogger) WarnUnsupportedVersion(_, _, _ int) {} func (n noopLogger) PrepareUpgrade(_, _, _ int) {} func (n noopLogger) DoUpgrade(_, _ int, _ string, _ bool) {} -func (n noopLogger) Warn(msg string, args ...interface{}) {} +func (n noopLogger) Warn(msg string, args ...any) {} -func (n noopLogger) QueryTiming(_ context.Context, _, _ string, _ []interface{}, _ int, _ time.Duration, _ error) { +func (n noopLogger) QueryTiming(_ context.Context, _, _ string, _ []any, _ int, _ time.Duration, _ error) { } type zeroLogger struct { @@ -92,7 +92,7 @@ func (z zeroLogger) DoUpgrade(from, to int, message string, txn bool) { var whitespaceRegex = regexp.MustCompile(`\s+`) -func (z zeroLogger) QueryTiming(ctx context.Context, method, query string, args []interface{}, nrows int, duration time.Duration, err error) { +func (z zeroLogger) QueryTiming(ctx context.Context, method, query string, args []any, nrows int, duration time.Duration, err error) { log := zerolog.Ctx(ctx) if log.GetLevel() == zerolog.Disabled || log == zerolog.DefaultContextLogger { log = z.l @@ -124,6 +124,6 @@ func (z zeroLogger) QueryTiming(ctx context.Context, method, query string, args } } -func (z zeroLogger) Warn(msg string, args ...interface{}) { - z.l.Warn().Msgf(msg, args...) +func (z zeroLogger) Warn(msg string, args ...any) { + z.l.Warn().Msgf(msg, args...) // zerolog-allow-msgf } diff --git a/vendor/go.mau.fi/util/dbutil/queryhelper.go b/vendor/go.mau.fi/util/dbutil/queryhelper.go new file mode 100644 index 0000000..16e5fd3 --- /dev/null +++ b/vendor/go.mau.fi/util/dbutil/queryhelper.go @@ -0,0 +1,105 @@ +// Copyright (c) 2023 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package dbutil + +import ( + "context" + "database/sql" + "errors" + + "golang.org/x/exp/constraints" +) + +// DataStruct is an interface for structs that represent a single database row. +type DataStruct[T any] interface { + Scan(row Scannable) (T, error) +} + +// QueryHelper is a generic helper struct for SQL query execution boilerplate. +// +// After implementing the Scan and Init methods in a data struct, the query +// helper allows writing query functions in a single line. +type QueryHelper[T DataStruct[T]] struct { + db *Database + newFunc func(qh *QueryHelper[T]) T +} + +func MakeQueryHelper[T DataStruct[T]](db *Database, new func(qh *QueryHelper[T]) T) *QueryHelper[T] { + return &QueryHelper[T]{db: db, newFunc: new} +} + +// ValueOrErr is a helper function that returns the value if err is nil, or +// returns nil and the error if err is not nil. It can be used to avoid +// `if err != nil { return nil, err }` boilerplate in certain cases like +// DataStruct.Scan implementations. +func ValueOrErr[T any](val *T, err error) (*T, error) { + if err != nil { + return nil, err + } + return val, nil +} + +// StrPtr returns a pointer to the given string, or nil if the string is empty. +func StrPtr[T ~string](val T) *string { + if val == "" { + return nil + } + strVal := string(val) + return &strVal +} + +// NumPtr returns a pointer to the given number, or nil if the number is zero. +func NumPtr[T constraints.Integer | constraints.Float](val T) *T { + if val == 0 { + return nil + } + return &val +} + +func (qh *QueryHelper[T]) GetDB() *Database { + return qh.db +} + +func (qh *QueryHelper[T]) New() T { + return qh.newFunc(qh) +} + +// Exec executes a query with ExecContext and returns the error. +// +// It omits the sql.Result return value, as it is rarely used. When the result +// is wanted, use `qh.GetDB().Exec(...)` instead, which is +// otherwise equivalent. +func (qh *QueryHelper[T]) Exec(ctx context.Context, query string, args ...any) error { + _, err := qh.db.Exec(ctx, query, args...) + return err +} + +func (qh *QueryHelper[T]) scanNew(row Scannable) (T, error) { + return qh.New().Scan(row) +} + +// QueryOne executes a query with QueryRowContext, uses the associated DataStruct +// to scan it, and returns the value. If the query returns no rows, it returns nil +// and no error. +func (qh *QueryHelper[T]) QueryOne(ctx context.Context, query string, args ...any) (val T, err error) { + val, err = qh.scanNew(qh.db.QueryRow(ctx, query, args...)) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + return val, err +} + +// QueryMany executes a query with QueryContext, uses the associated DataStruct +// to scan each row, and returns the values. If the query returns no rows, it +// returns a non-nil zero-length slice and no error. +func (qh *QueryHelper[T]) QueryMany(ctx context.Context, query string, args ...any) ([]T, error) { + rows, err := qh.db.Query(ctx, query, args...) + if err != nil { + return nil, err + } + return NewRowIter(rows, qh.scanNew).AsList() +} diff --git a/vendor/go.mau.fi/util/dbutil/transaction.go b/vendor/go.mau.fi/util/dbutil/transaction.go index 11f7189..2569b4a 100644 --- a/vendor/go.mau.fi/util/dbutil/transaction.go +++ b/vendor/go.mau.fi/util/dbutil/transaction.go @@ -32,6 +32,22 @@ const ( ContextKeyDoTxnCallerSkip ) +func (db *Database) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) { + return db.Conn(ctx).ExecContext(ctx, query, args...) +} + +func (db *Database) Query(ctx context.Context, query string, args ...any) (Rows, error) { + return db.Conn(ctx).QueryContext(ctx, query, args...) +} + +func (db *Database) QueryRow(ctx context.Context, query string, args ...any) *sql.Row { + return db.Conn(ctx).QueryRowContext(ctx, query, args...) +} + +func (db *Database) BeginTx(ctx context.Context, opts *sql.TxOptions) (*LoggingTxn, error) { + return db.LoggingDB.BeginTx(ctx, opts) +} + func (db *Database) DoTxn(ctx context.Context, opts *sql.TxOptions, fn func(ctx context.Context) error) error { if ctx.Value(ContextKeyDatabaseTransaction) != nil { zerolog.Ctx(ctx).Trace().Msg("Already in a transaction, not creating a new one") @@ -82,13 +98,13 @@ func (db *Database) DoTxn(ctx context.Context, opts *sql.TxOptions, fn func(ctx return nil } -func (db *Database) Conn(ctx context.Context) ContextExecable { +func (db *Database) Conn(ctx context.Context) Execable { if ctx == nil { - return db + return &db.LoggingDB } txn, ok := ctx.Value(ContextKeyDatabaseTransaction).(Transaction) if ok { return txn } - return db + return &db.LoggingDB } diff --git a/vendor/go.mau.fi/util/dbutil/upgrades.go b/vendor/go.mau.fi/util/dbutil/upgrades.go index 735e2b3..2f2de7c 100644 --- a/vendor/go.mau.fi/util/dbutil/upgrades.go +++ b/vendor/go.mau.fi/util/dbutil/upgrades.go @@ -7,12 +7,13 @@ package dbutil import ( + "context" "database/sql" "errors" "fmt" ) -type upgradeFunc func(Execable, *Database) error +type upgradeFunc func(context.Context, *Database) error type upgrade struct { message string @@ -28,19 +29,19 @@ var ErrForeignTables = errors.New("the database contains foreign tables") var ErrNotOwned = errors.New("the database is owned by") var ErrUnsupportedDialect = errors.New("unsupported database dialect") -func (db *Database) upgradeVersionTable() error { - if compatColumnExists, err := db.ColumnExists(nil, db.VersionTable, "compat"); err != nil { +func (db *Database) upgradeVersionTable(ctx context.Context) error { + if compatColumnExists, err := db.ColumnExists(ctx, db.VersionTable, "compat"); err != nil { return fmt.Errorf("failed to check if version table is up to date: %w", err) } else if !compatColumnExists { - if tableExists, err := db.TableExists(nil, db.VersionTable); err != nil { + if tableExists, err := db.TableExists(ctx, db.VersionTable); err != nil { return fmt.Errorf("failed to check if version table exists: %w", err) } else if !tableExists { - _, err = db.Exec(fmt.Sprintf("CREATE TABLE %s (version INTEGER, compat INTEGER)", db.VersionTable)) + _, err = db.Exec(ctx, fmt.Sprintf("CREATE TABLE %s (version INTEGER, compat INTEGER)", db.VersionTable)) if err != nil { return fmt.Errorf("failed to create version table: %w", err) } } else { - _, err = db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN compat INTEGER", db.VersionTable)) + _, err = db.Exec(ctx, fmt.Sprintf("ALTER TABLE %s ADD COLUMN compat INTEGER", db.VersionTable)) if err != nil { return fmt.Errorf("failed to add compat column to version table: %w", err) } @@ -49,13 +50,13 @@ func (db *Database) upgradeVersionTable() error { return nil } -func (db *Database) getVersion() (version, compat int, err error) { - if err = db.upgradeVersionTable(); err != nil { +func (db *Database) getVersion(ctx context.Context) (version, compat int, err error) { + if err = db.upgradeVersionTable(ctx); err != nil { return } var compatNull sql.NullInt32 - err = db.QueryRow(fmt.Sprintf("SELECT version, compat FROM %s LIMIT 1", db.VersionTable)).Scan(&version, &compatNull) + err = db.QueryRow(ctx, fmt.Sprintf("SELECT version, compat FROM %s LIMIT 1", db.VersionTable)).Scan(&version, &compatNull) if errors.Is(err, sql.ErrNoRows) { err = nil } @@ -72,15 +73,12 @@ const ( tableExistsSQLite = "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND tbl_name=?1)" ) -func (db *Database) TableExists(tx Execable, table string) (exists bool, err error) { - if tx == nil { - tx = db - } +func (db *Database) TableExists(ctx context.Context, table string) (exists bool, err error) { switch db.Dialect { case SQLite: - err = db.QueryRow(tableExistsSQLite, table).Scan(&exists) + err = db.QueryRow(ctx, tableExistsSQLite, table).Scan(&exists) case Postgres: - err = db.QueryRow(tableExistsPostgres, table).Scan(&exists) + err = db.QueryRow(ctx, tableExistsPostgres, table).Scan(&exists) default: err = ErrUnsupportedDialect } @@ -92,23 +90,20 @@ const ( columnExistsSQLite = "SELECT EXISTS(SELECT 1 FROM pragma_table_info(?1) WHERE name=?2)" ) -func (db *Database) ColumnExists(tx Execable, table, column string) (exists bool, err error) { - if tx == nil { - tx = db - } +func (db *Database) ColumnExists(ctx context.Context, table, column string) (exists bool, err error) { switch db.Dialect { case SQLite: - err = db.QueryRow(columnExistsSQLite, table, column).Scan(&exists) + err = db.QueryRow(ctx, columnExistsSQLite, table, column).Scan(&exists) case Postgres: - err = db.QueryRow(columnExistsPostgres, table, column).Scan(&exists) + err = db.QueryRow(ctx, columnExistsPostgres, table, column).Scan(&exists) default: err = ErrUnsupportedDialect } return } -func (db *Database) tableExistsNoError(table string) bool { - exists, err := db.TableExists(nil, table) +func (db *Database) tableExistsNoError(ctx context.Context, table string) bool { + exists, err := db.TableExists(ctx, table) if err != nil { panic(fmt.Errorf("failed to check if table exists: %w", err)) } @@ -122,22 +117,22 @@ CREATE TABLE IF NOT EXISTS database_owner ( ) ` -func (db *Database) checkDatabaseOwner() error { +func (db *Database) checkDatabaseOwner(ctx context.Context) error { var owner string if !db.IgnoreForeignTables { - if db.tableExistsNoError("state_groups_state") { + if db.tableExistsNoError(ctx, "state_groups_state") { return fmt.Errorf("%w (found state_groups_state, likely belonging to Synapse)", ErrForeignTables) - } else if db.tableExistsNoError("roomserver_rooms") { + } else if db.tableExistsNoError(ctx, "roomserver_rooms") { return fmt.Errorf("%w (found roomserver_rooms, likely belonging to Dendrite)", ErrForeignTables) } } if db.Owner == "" { return nil } - if _, err := db.Exec(createOwnerTable); err != nil { + if _, err := db.Exec(ctx, createOwnerTable); err != nil { return fmt.Errorf("failed to ensure database owner table exists: %w", err) - } else if err = db.QueryRow("SELECT owner FROM database_owner WHERE key=0").Scan(&owner); errors.Is(err, sql.ErrNoRows) { - _, err = db.Exec("INSERT INTO database_owner (key, owner) VALUES (0, $1)", db.Owner) + } else if err = db.QueryRow(ctx, "SELECT owner FROM database_owner WHERE key=0").Scan(&owner); errors.Is(err, sql.ErrNoRows) { + _, err = db.Exec(ctx, "INSERT INTO database_owner (key, owner) VALUES (0, $1)", db.Owner) if err != nil { return fmt.Errorf("failed to insert database owner: %w", err) } @@ -149,22 +144,22 @@ func (db *Database) checkDatabaseOwner() error { return nil } -func (db *Database) setVersion(tx Execable, version, compat int) error { - _, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", db.VersionTable)) +func (db *Database) setVersion(ctx context.Context, version, compat int) error { + _, err := db.Exec(ctx, fmt.Sprintf("DELETE FROM %s", db.VersionTable)) if err != nil { return err } - _, err = tx.Exec(fmt.Sprintf("INSERT INTO %s (version, compat) VALUES ($1, $2)", db.VersionTable), version, compat) + _, err = db.Exec(ctx, fmt.Sprintf("INSERT INTO %s (version, compat) VALUES ($1, $2)", db.VersionTable), version, compat) return err } -func (db *Database) Upgrade() error { - err := db.checkDatabaseOwner() +func (db *Database) Upgrade(ctx context.Context) error { + err := db.checkDatabaseOwner(ctx) if err != nil { return err } - version, compat, err := db.getVersion() + version, compat, err := db.getVersion(ctx) if err != nil { return err } @@ -185,34 +180,28 @@ func (db *Database) Upgrade() error { version++ continue } + doUpgrade := func(ctx context.Context) error { + err = upgradeItem.fn(ctx, db) + if err != nil { + return fmt.Errorf("failed to run upgrade #%d: %w", version, err) + } + version = upgradeItem.upgradesTo + logVersion = version + err = db.setVersion(ctx, version, upgradeItem.compatVersion) + if err != nil { + return err + } + return nil + } db.Log.DoUpgrade(logVersion, upgradeItem.upgradesTo, upgradeItem.message, upgradeItem.transaction) - var tx Transaction - var upgradeConn Execable if upgradeItem.transaction { - tx, err = db.Begin() - if err != nil { - return err - } - upgradeConn = tx + err = db.DoTxn(ctx, nil, doUpgrade) } else { - upgradeConn = db + err = doUpgrade(ctx) } - err = upgradeItem.fn(upgradeConn, db) if err != nil { return err } - version = upgradeItem.upgradesTo - logVersion = version - err = db.setVersion(upgradeConn, version, upgradeItem.compatVersion) - if err != nil { - return err - } - if tx != nil { - err = tx.Commit() - if err != nil { - return err - } - } } return nil } diff --git a/vendor/go.mau.fi/util/dbutil/upgradetable.go b/vendor/go.mau.fi/util/dbutil/upgradetable.go index c3bd841..0e0d2c4 100644 --- a/vendor/go.mau.fi/util/dbutil/upgradetable.go +++ b/vendor/go.mau.fi/util/dbutil/upgradetable.go @@ -8,6 +8,7 @@ package dbutil import ( "bytes" + "context" "errors" "fmt" "io/fs" @@ -189,27 +190,27 @@ func (db *Database) filterSQLUpgrade(lines [][]byte) (string, error) { } func sqlUpgradeFunc(fileName string, lines [][]byte) upgradeFunc { - return func(tx Execable, db *Database) error { + return func(ctx context.Context, db *Database) error { if skip, err := db.parseDialectFilter(lines[0]); err == nil && skip == skipNextLine { return nil } else if upgradeSQL, err := db.filterSQLUpgrade(lines); err != nil { panic(fmt.Errorf("failed to parse upgrade %s: %w", fileName, err)) } else { - _, err = tx.Exec(upgradeSQL) + _, err = db.Exec(ctx, upgradeSQL) return err } } } func splitSQLUpgradeFunc(sqliteData, postgresData string) upgradeFunc { - return func(tx Execable, database *Database) (err error) { - switch database.Dialect { + return func(ctx context.Context, db *Database) (err error) { + switch db.Dialect { case SQLite: - _, err = tx.Exec(sqliteData) + _, err = db.Exec(ctx, sqliteData) case Postgres: - _, err = tx.Exec(postgresData) + _, err = db.Exec(ctx, postgresData) default: - err = fmt.Errorf("unknown dialect %s", database.Dialect) + err = fmt.Errorf("unknown dialect %s", db.Dialect) } return } diff --git a/vendor/go.mau.fi/util/exerrors/must.go b/vendor/go.mau.fi/util/exerrors/must.go new file mode 100644 index 0000000..2ffda30 --- /dev/null +++ b/vendor/go.mau.fi/util/exerrors/must.go @@ -0,0 +1,23 @@ +// Copyright (c) 2024 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package exerrors + +func Must[T any](val T, err error) T { + PanicIfNotNil(err) + return val +} + +func Must2[T any, T2 any](val T, val2 T2, err error) (T, T2) { + PanicIfNotNil(err) + return val, val2 +} + +func PanicIfNotNil(err error) { + if err != nil { + panic(err) + } +} diff --git a/vendor/go.mau.fi/util/jsontime/integer.go b/vendor/go.mau.fi/util/jsontime/integer.go index 26ac588..7d15d5d 100644 --- a/vendor/go.mau.fi/util/jsontime/integer.go +++ b/vendor/go.mau.fi/util/jsontime/integer.go @@ -7,10 +7,16 @@ package jsontime import ( + "database/sql" + "database/sql/driver" "encoding/json" + "errors" + "fmt" "time" ) +var ErrNotInteger = errors.New("value is not an integer") + func parseTime(data []byte, unixConv func(int64) time.Time, into *time.Time) error { var val int64 err := json.Unmarshal(data, &val) @@ -25,6 +31,28 @@ func parseTime(data []byte, unixConv func(int64) time.Time, into *time.Time) err return nil } +func anyIntegerToTime(src any, unixConv func(int64) time.Time, into *time.Time) error { + switch v := src.(type) { + case int: + *into = unixConv(int64(v)) + case int8: + *into = unixConv(int64(v)) + case int16: + *into = unixConv(int64(v)) + case int32: + *into = unixConv(int64(v)) + case int64: + *into = unixConv(int64(v)) + default: + return fmt.Errorf("%w: %T", ErrNotInteger, src) + } + + return nil +} + +var _ sql.Scanner = &UnixMilli{} +var _ driver.Valuer = UnixMilli{} + type UnixMilli struct { time.Time } @@ -40,6 +68,17 @@ func (um *UnixMilli) UnmarshalJSON(data []byte) error { return parseTime(data, time.UnixMilli, &um.Time) } +func (um UnixMilli) Value() (driver.Value, error) { + return um.UnixMilli(), nil +} + +func (um *UnixMilli) Scan(src any) error { + return anyIntegerToTime(src, time.UnixMilli, &um.Time) +} + +var _ sql.Scanner = &UnixMicro{} +var _ driver.Valuer = UnixMicro{} + type UnixMicro struct { time.Time } @@ -55,6 +94,17 @@ func (um *UnixMicro) UnmarshalJSON(data []byte) error { return parseTime(data, time.UnixMicro, &um.Time) } +func (um UnixMicro) Value() (driver.Value, error) { + return um.UnixMicro(), nil +} + +func (um *UnixMicro) Scan(src any) error { + return anyIntegerToTime(src, time.UnixMicro, &um.Time) +} + +var _ sql.Scanner = &UnixNano{} +var _ driver.Valuer = UnixNano{} + type UnixNano struct { time.Time } @@ -72,6 +122,16 @@ func (un *UnixNano) UnmarshalJSON(data []byte) error { }, &un.Time) } +func (un UnixNano) Value() (driver.Value, error) { + return un.UnixNano(), nil +} + +func (un *UnixNano) Scan(src any) error { + return anyIntegerToTime(src, func(i int64) time.Time { + return time.Unix(0, i) + }, &un.Time) +} + type Unix struct { time.Time } @@ -83,8 +143,21 @@ func (u Unix) MarshalJSON() ([]byte, error) { return json.Marshal(u.Unix()) } +var _ sql.Scanner = &Unix{} +var _ driver.Valuer = Unix{} + func (u *Unix) UnmarshalJSON(data []byte) error { return parseTime(data, func(i int64) time.Time { return time.Unix(i, 0) }, &u.Time) } + +func (u Unix) Value() (driver.Value, error) { + return u.Unix(), nil +} + +func (u *Unix) Scan(src any) error { + return anyIntegerToTime(src, func(i int64) time.Time { + return time.Unix(i, 0) + }, &u.Time) +} diff --git a/vendor/golang.org/x/crypto/argon2/blamka_amd64.s b/vendor/golang.org/x/crypto/argon2/blamka_amd64.s index f3b653a..6713acc 100644 --- a/vendor/golang.org/x/crypto/argon2/blamka_amd64.s +++ b/vendor/golang.org/x/crypto/argon2/blamka_amd64.s @@ -199,8 +199,8 @@ TEXT ·mixBlocksSSE2(SB), 4, $0-32 MOVQ out+0(FP), DX MOVQ a+8(FP), AX MOVQ b+16(FP), BX - MOVQ a+24(FP), CX - MOVQ $128, BP + MOVQ c+24(FP), CX + MOVQ $128, DI loop: MOVOU 0(AX), X0 @@ -213,7 +213,7 @@ loop: ADDQ $16, BX ADDQ $16, CX ADDQ $16, DX - SUBQ $2, BP + SUBQ $2, DI JA loop RET @@ -222,8 +222,8 @@ TEXT ·xorBlocksSSE2(SB), 4, $0-32 MOVQ out+0(FP), DX MOVQ a+8(FP), AX MOVQ b+16(FP), BX - MOVQ a+24(FP), CX - MOVQ $128, BP + MOVQ c+24(FP), CX + MOVQ $128, DI loop: MOVOU 0(AX), X0 @@ -238,6 +238,6 @@ loop: ADDQ $16, BX ADDQ $16, CX ADDQ $16, DX - SUBQ $2, BP + SUBQ $2, DI JA loop RET diff --git a/vendor/golang.org/x/crypto/blake2b/blake2bAVX2_amd64.go b/vendor/golang.org/x/crypto/blake2b/blake2bAVX2_amd64.go index 4f506f8..199c21d 100644 --- a/vendor/golang.org/x/crypto/blake2b/blake2bAVX2_amd64.go +++ b/vendor/golang.org/x/crypto/blake2b/blake2bAVX2_amd64.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.7 && amd64 && gc && !purego +//go:build amd64 && gc && !purego package blake2b diff --git a/vendor/golang.org/x/crypto/blake2b/blake2bAVX2_amd64.s b/vendor/golang.org/x/crypto/blake2b/blake2bAVX2_amd64.s index 353bb7c..9ae8206 100644 --- a/vendor/golang.org/x/crypto/blake2b/blake2bAVX2_amd64.s +++ b/vendor/golang.org/x/crypto/blake2b/blake2bAVX2_amd64.s @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.7 && amd64 && gc && !purego +//go:build amd64 && gc && !purego #include "textflag.h" diff --git a/vendor/golang.org/x/crypto/blake2b/blake2b_amd64.go b/vendor/golang.org/x/crypto/blake2b/blake2b_amd64.go deleted file mode 100644 index 1d0770a..0000000 --- a/vendor/golang.org/x/crypto/blake2b/blake2b_amd64.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.7 && amd64 && gc && !purego - -package blake2b - -import "golang.org/x/sys/cpu" - -func init() { - useSSE4 = cpu.X86.HasSSE41 -} - -//go:noescape -func hashBlocksSSE4(h *[8]uint64, c *[2]uint64, flag uint64, blocks []byte) - -func hashBlocks(h *[8]uint64, c *[2]uint64, flag uint64, blocks []byte) { - if useSSE4 { - hashBlocksSSE4(h, c, flag, blocks) - } else { - hashBlocksGeneric(h, c, flag, blocks) - } -} diff --git a/vendor/golang.org/x/crypto/blake2b/register.go b/vendor/golang.org/x/crypto/blake2b/register.go index d9fcac3..54e446e 100644 --- a/vendor/golang.org/x/crypto/blake2b/register.go +++ b/vendor/golang.org/x/crypto/blake2b/register.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.9 - package blake2b import ( diff --git a/vendor/golang.org/x/crypto/internal/poly1305/bits_compat.go b/vendor/golang.org/x/crypto/internal/poly1305/bits_compat.go deleted file mode 100644 index d33c889..0000000 --- a/vendor/golang.org/x/crypto/internal/poly1305/bits_compat.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.13 - -package poly1305 - -// Generic fallbacks for the math/bits intrinsics, copied from -// src/math/bits/bits.go. They were added in Go 1.12, but Add64 and Sum64 had -// variable time fallbacks until Go 1.13. - -func bitsAdd64(x, y, carry uint64) (sum, carryOut uint64) { - sum = x + y + carry - carryOut = ((x & y) | ((x | y) &^ sum)) >> 63 - return -} - -func bitsSub64(x, y, borrow uint64) (diff, borrowOut uint64) { - diff = x - y - borrow - borrowOut = ((^x & y) | (^(x ^ y) & diff)) >> 63 - return -} - -func bitsMul64(x, y uint64) (hi, lo uint64) { - const mask32 = 1<<32 - 1 - x0 := x & mask32 - x1 := x >> 32 - y0 := y & mask32 - y1 := y >> 32 - w0 := x0 * y0 - t := x1*y0 + w0>>32 - w1 := t & mask32 - w2 := t >> 32 - w1 += x0 * y1 - hi = x1*y1 + w2 + w1>>32 - lo = x * y - return -} diff --git a/vendor/golang.org/x/crypto/internal/poly1305/bits_go1.13.go b/vendor/golang.org/x/crypto/internal/poly1305/bits_go1.13.go deleted file mode 100644 index 495c1fa..0000000 --- a/vendor/golang.org/x/crypto/internal/poly1305/bits_go1.13.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.13 - -package poly1305 - -import "math/bits" - -func bitsAdd64(x, y, carry uint64) (sum, carryOut uint64) { - return bits.Add64(x, y, carry) -} - -func bitsSub64(x, y, borrow uint64) (diff, borrowOut uint64) { - return bits.Sub64(x, y, borrow) -} - -func bitsMul64(x, y uint64) (hi, lo uint64) { - return bits.Mul64(x, y) -} diff --git a/vendor/golang.org/x/crypto/internal/poly1305/sum_generic.go b/vendor/golang.org/x/crypto/internal/poly1305/sum_generic.go index e041da5..ec2202b 100644 --- a/vendor/golang.org/x/crypto/internal/poly1305/sum_generic.go +++ b/vendor/golang.org/x/crypto/internal/poly1305/sum_generic.go @@ -7,7 +7,10 @@ package poly1305 -import "encoding/binary" +import ( + "encoding/binary" + "math/bits" +) // Poly1305 [RFC 7539] is a relatively simple algorithm: the authentication tag // for a 64 bytes message is approximately @@ -114,13 +117,13 @@ type uint128 struct { } func mul64(a, b uint64) uint128 { - hi, lo := bitsMul64(a, b) + hi, lo := bits.Mul64(a, b) return uint128{lo, hi} } func add128(a, b uint128) uint128 { - lo, c := bitsAdd64(a.lo, b.lo, 0) - hi, c := bitsAdd64(a.hi, b.hi, c) + lo, c := bits.Add64(a.lo, b.lo, 0) + hi, c := bits.Add64(a.hi, b.hi, c) if c != 0 { panic("poly1305: unexpected overflow") } @@ -155,8 +158,8 @@ func updateGeneric(state *macState, msg []byte) { // hide leading zeroes. For full chunks, that's 1 << 128, so we can just // add 1 to the most significant (2¹²⁸) limb, h2. if len(msg) >= TagSize { - h0, c = bitsAdd64(h0, binary.LittleEndian.Uint64(msg[0:8]), 0) - h1, c = bitsAdd64(h1, binary.LittleEndian.Uint64(msg[8:16]), c) + h0, c = bits.Add64(h0, binary.LittleEndian.Uint64(msg[0:8]), 0) + h1, c = bits.Add64(h1, binary.LittleEndian.Uint64(msg[8:16]), c) h2 += c + 1 msg = msg[TagSize:] @@ -165,8 +168,8 @@ func updateGeneric(state *macState, msg []byte) { copy(buf[:], msg) buf[len(msg)] = 1 - h0, c = bitsAdd64(h0, binary.LittleEndian.Uint64(buf[0:8]), 0) - h1, c = bitsAdd64(h1, binary.LittleEndian.Uint64(buf[8:16]), c) + h0, c = bits.Add64(h0, binary.LittleEndian.Uint64(buf[0:8]), 0) + h1, c = bits.Add64(h1, binary.LittleEndian.Uint64(buf[8:16]), c) h2 += c msg = nil @@ -219,9 +222,9 @@ func updateGeneric(state *macState, msg []byte) { m3 := h2r1 t0 := m0.lo - t1, c := bitsAdd64(m1.lo, m0.hi, 0) - t2, c := bitsAdd64(m2.lo, m1.hi, c) - t3, _ := bitsAdd64(m3.lo, m2.hi, c) + t1, c := bits.Add64(m1.lo, m0.hi, 0) + t2, c := bits.Add64(m2.lo, m1.hi, c) + t3, _ := bits.Add64(m3.lo, m2.hi, c) // Now we have the result as 4 64-bit limbs, and we need to reduce it // modulo 2¹³⁰ - 5. The special shape of this Crandall prime lets us do @@ -243,14 +246,14 @@ func updateGeneric(state *macState, msg []byte) { // To add c * 5 to h, we first add cc = c * 4, and then add (cc >> 2) = c. - h0, c = bitsAdd64(h0, cc.lo, 0) - h1, c = bitsAdd64(h1, cc.hi, c) + h0, c = bits.Add64(h0, cc.lo, 0) + h1, c = bits.Add64(h1, cc.hi, c) h2 += c cc = shiftRightBy2(cc) - h0, c = bitsAdd64(h0, cc.lo, 0) - h1, c = bitsAdd64(h1, cc.hi, c) + h0, c = bits.Add64(h0, cc.lo, 0) + h1, c = bits.Add64(h1, cc.hi, c) h2 += c // h2 is at most 3 + 1 + 1 = 5, making the whole of h at most @@ -287,9 +290,9 @@ func finalize(out *[TagSize]byte, h *[3]uint64, s *[2]uint64) { // in constant time, we compute t = h - (2¹³⁰ - 5), and select h as the // result if the subtraction underflows, and t otherwise. - hMinusP0, b := bitsSub64(h0, p0, 0) - hMinusP1, b := bitsSub64(h1, p1, b) - _, b = bitsSub64(h2, p2, b) + hMinusP0, b := bits.Sub64(h0, p0, 0) + hMinusP1, b := bits.Sub64(h1, p1, b) + _, b = bits.Sub64(h2, p2, b) // h = h if h < p else h - p h0 = select64(b, h0, hMinusP0) @@ -301,8 +304,8 @@ func finalize(out *[TagSize]byte, h *[3]uint64, s *[2]uint64) { // // by just doing a wide addition with the 128 low bits of h and discarding // the overflow. - h0, c := bitsAdd64(h0, s[0], 0) - h1, _ = bitsAdd64(h1, s[1], c) + h0, c := bits.Add64(h0, s[0], 0) + h1, _ = bits.Add64(h1, s[1], c) binary.LittleEndian.PutUint64(out[0:8], h0) binary.LittleEndian.PutUint64(out[8:16], h1) diff --git a/vendor/golang.org/x/crypto/ssh/channel.go b/vendor/golang.org/x/crypto/ssh/channel.go index c0834c0..cc0bb7a 100644 --- a/vendor/golang.org/x/crypto/ssh/channel.go +++ b/vendor/golang.org/x/crypto/ssh/channel.go @@ -187,9 +187,11 @@ type channel struct { pending *buffer extPending *buffer - // windowMu protects myWindow, the flow-control window. - windowMu sync.Mutex - myWindow uint32 + // windowMu protects myWindow, the flow-control window, and myConsumed, + // the number of bytes consumed since we last increased myWindow + windowMu sync.Mutex + myWindow uint32 + myConsumed uint32 // writeMu serializes calls to mux.conn.writePacket() and // protects sentClose and packetPool. This mutex must be @@ -332,14 +334,24 @@ func (ch *channel) handleData(packet []byte) error { return nil } -func (c *channel) adjustWindow(n uint32) error { +func (c *channel) adjustWindow(adj uint32) error { c.windowMu.Lock() - // Since myWindow is managed on our side, and can never exceed - // the initial window setting, we don't worry about overflow. - c.myWindow += uint32(n) + // Since myConsumed and myWindow are managed on our side, and can never + // exceed the initial window setting, we don't worry about overflow. + c.myConsumed += adj + var sendAdj uint32 + if (channelWindowSize-c.myWindow > 3*c.maxIncomingPayload) || + (c.myWindow < channelWindowSize/2) { + sendAdj = c.myConsumed + c.myConsumed = 0 + c.myWindow += sendAdj + } c.windowMu.Unlock() + if sendAdj == 0 { + return nil + } return c.sendMessage(windowAdjustMsg{ - AdditionalBytes: uint32(n), + AdditionalBytes: sendAdj, }) } diff --git a/vendor/golang.org/x/crypto/ssh/client.go b/vendor/golang.org/x/crypto/ssh/client.go index bdc356c..fd8c497 100644 --- a/vendor/golang.org/x/crypto/ssh/client.go +++ b/vendor/golang.org/x/crypto/ssh/client.go @@ -82,7 +82,7 @@ func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan if err := conn.clientHandshake(addr, &fullConf); err != nil { c.Close() - return nil, nil, nil, fmt.Errorf("ssh: handshake failed: %v", err) + return nil, nil, nil, fmt.Errorf("ssh: handshake failed: %w", err) } conn.mux = newMux(conn.transport) return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil diff --git a/vendor/golang.org/x/crypto/ssh/client_auth.go b/vendor/golang.org/x/crypto/ssh/client_auth.go index 5c3bc25..34bf089 100644 --- a/vendor/golang.org/x/crypto/ssh/client_auth.go +++ b/vendor/golang.org/x/crypto/ssh/client_auth.go @@ -307,7 +307,10 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand } var methods []string var errSigAlgo error - for _, signer := range signers { + + origSignersLen := len(signers) + for idx := 0; idx < len(signers); idx++ { + signer := signers[idx] pub := signer.PublicKey() as, algo, err := pickSignatureAlgorithm(signer, extensions) if err != nil && errSigAlgo == nil { @@ -321,6 +324,21 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand if err != nil { return authFailure, nil, err } + // OpenSSH 7.2-7.7 advertises support for rsa-sha2-256 and rsa-sha2-512 + // in the "server-sig-algs" extension but doesn't support these + // algorithms for certificate authentication, so if the server rejects + // the key try to use the obtained algorithm as if "server-sig-algs" had + // not been implemented if supported from the algorithm signer. + if !ok && idx < origSignersLen && isRSACert(algo) && algo != CertAlgoRSAv01 { + if contains(as.Algorithms(), KeyAlgoRSA) { + // We retry using the compat algorithm after all signers have + // been tried normally. + signers = append(signers, &multiAlgorithmSigner{ + AlgorithmSigner: as, + supportedAlgorithms: []string{KeyAlgoRSA}, + }) + } + } if !ok { continue } diff --git a/vendor/golang.org/x/crypto/ssh/common.go b/vendor/golang.org/x/crypto/ssh/common.go index dd2ab0d..7e9c2cb 100644 --- a/vendor/golang.org/x/crypto/ssh/common.go +++ b/vendor/golang.org/x/crypto/ssh/common.go @@ -127,6 +127,14 @@ func isRSA(algo string) bool { return contains(algos, underlyingAlgo(algo)) } +func isRSACert(algo string) bool { + _, ok := certKeyAlgoNames[algo] + if !ok { + return false + } + return isRSA(algo) +} + // supportedPubKeyAuthAlgos specifies the supported client public key // authentication algorithms. Note that this doesn't include certificate types // since those use the underlying algorithm. This list is sent to the client if diff --git a/vendor/golang.org/x/crypto/ssh/handshake.go b/vendor/golang.org/x/crypto/ssh/handshake.go index 49bbba7..56cdc7c 100644 --- a/vendor/golang.org/x/crypto/ssh/handshake.go +++ b/vendor/golang.org/x/crypto/ssh/handshake.go @@ -35,6 +35,16 @@ type keyingTransport interface { // direction will be effected if a msgNewKeys message is sent // or received. prepareKeyChange(*algorithms, *kexResult) error + + // setStrictMode sets the strict KEX mode, notably triggering + // sequence number resets on sending or receiving msgNewKeys. + // If the sequence number is already > 1 when setStrictMode + // is called, an error is returned. + setStrictMode() error + + // setInitialKEXDone indicates to the transport that the initial key exchange + // was completed + setInitialKEXDone() } // handshakeTransport implements rekeying on top of a keyingTransport @@ -100,6 +110,10 @@ type handshakeTransport struct { // The session ID or nil if first kex did not complete yet. sessionID []byte + + // strictMode indicates if the other side of the handshake indicated + // that we should be following the strict KEX protocol restrictions. + strictMode bool } type pendingKex struct { @@ -209,7 +223,10 @@ func (t *handshakeTransport) readLoop() { close(t.incoming) break } - if p[0] == msgIgnore || p[0] == msgDebug { + // If this is the first kex, and strict KEX mode is enabled, + // we don't ignore any messages, as they may be used to manipulate + // the packet sequence numbers. + if !(t.sessionID == nil && t.strictMode) && (p[0] == msgIgnore || p[0] == msgDebug) { continue } t.incoming <- p @@ -441,6 +458,11 @@ func (t *handshakeTransport) readOnePacket(first bool) ([]byte, error) { return successPacket, nil } +const ( + kexStrictClient = "kex-strict-c-v00@openssh.com" + kexStrictServer = "kex-strict-s-v00@openssh.com" +) + // sendKexInit sends a key change message. func (t *handshakeTransport) sendKexInit() error { t.mu.Lock() @@ -454,7 +476,6 @@ func (t *handshakeTransport) sendKexInit() error { } msg := &kexInitMsg{ - KexAlgos: t.config.KeyExchanges, CiphersClientServer: t.config.Ciphers, CiphersServerClient: t.config.Ciphers, MACsClientServer: t.config.MACs, @@ -464,6 +485,13 @@ func (t *handshakeTransport) sendKexInit() error { } io.ReadFull(rand.Reader, msg.Cookie[:]) + // We mutate the KexAlgos slice, in order to add the kex-strict extension algorithm, + // and possibly to add the ext-info extension algorithm. Since the slice may be the + // user owned KeyExchanges, we create our own slice in order to avoid using user + // owned memory by mistake. + msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+2) // room for kex-strict and ext-info + msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...) + isServer := len(t.hostKeys) > 0 if isServer { for _, k := range t.hostKeys { @@ -488,17 +516,24 @@ func (t *handshakeTransport) sendKexInit() error { msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, keyFormat) } } + + if t.sessionID == nil { + msg.KexAlgos = append(msg.KexAlgos, kexStrictServer) + } } else { msg.ServerHostKeyAlgos = t.hostKeyAlgorithms // As a client we opt in to receiving SSH_MSG_EXT_INFO so we know what // algorithms the server supports for public key authentication. See RFC // 8308, Section 2.1. + // + // We also send the strict KEX mode extension algorithm, in order to opt + // into the strict KEX mode. if firstKeyExchange := t.sessionID == nil; firstKeyExchange { - msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+1) - msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...) msg.KexAlgos = append(msg.KexAlgos, "ext-info-c") + msg.KexAlgos = append(msg.KexAlgos, kexStrictClient) } + } packet := Marshal(msg) @@ -604,6 +639,13 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error { return err } + if t.sessionID == nil && ((isClient && contains(serverInit.KexAlgos, kexStrictServer)) || (!isClient && contains(clientInit.KexAlgos, kexStrictClient))) { + t.strictMode = true + if err := t.conn.setStrictMode(); err != nil { + return err + } + } + // We don't send FirstKexFollows, but we handle receiving it. // // RFC 4253 section 7 defines the kex and the agreement method for @@ -679,6 +721,12 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error { return unexpectedMessageError(msgNewKeys, packet[0]) } + if firstKeyExchange { + // Indicates to the transport that the first key exchange is completed + // after receiving SSH_MSG_NEWKEYS. + t.conn.setInitialKEXDone() + } + return nil } diff --git a/vendor/golang.org/x/crypto/ssh/server.go b/vendor/golang.org/x/crypto/ssh/server.go index 8f1505a..c2dfe32 100644 --- a/vendor/golang.org/x/crypto/ssh/server.go +++ b/vendor/golang.org/x/crypto/ssh/server.go @@ -213,6 +213,7 @@ func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewCha } else { for _, algo := range fullConf.PublicKeyAuthAlgorithms { if !contains(supportedPubKeyAuthAlgos, algo) { + c.Close() return nil, nil, nil, fmt.Errorf("ssh: unsupported public key authentication algorithm %s", algo) } } @@ -220,6 +221,7 @@ func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewCha // Check if the config contains any unsupported key exchanges for _, kex := range fullConf.KeyExchanges { if _, ok := serverForbiddenKexAlgos[kex]; ok { + c.Close() return nil, nil, nil, fmt.Errorf("ssh: unsupported key exchange %s for server", kex) } } @@ -337,7 +339,7 @@ func checkSourceAddress(addr net.Addr, sourceAddrs string) error { return fmt.Errorf("ssh: remote address %v is not allowed because of source-address restriction", addr) } -func gssExchangeToken(gssapiConfig *GSSAPIWithMICConfig, firstToken []byte, s *connection, +func gssExchangeToken(gssapiConfig *GSSAPIWithMICConfig, token []byte, s *connection, sessionID []byte, userAuthReq userAuthRequestMsg) (authErr error, perms *Permissions, err error) { gssAPIServer := gssapiConfig.Server defer gssAPIServer.DeleteSecContext() @@ -347,7 +349,7 @@ func gssExchangeToken(gssapiConfig *GSSAPIWithMICConfig, firstToken []byte, s *c outToken []byte needContinue bool ) - outToken, srcName, needContinue, err = gssAPIServer.AcceptSecContext(firstToken) + outToken, srcName, needContinue, err = gssAPIServer.AcceptSecContext(token) if err != nil { return err, nil, nil } @@ -369,6 +371,7 @@ func gssExchangeToken(gssapiConfig *GSSAPIWithMICConfig, firstToken []byte, s *c if err := Unmarshal(packet, userAuthGSSAPITokenReq); err != nil { return nil, nil, err } + token = userAuthGSSAPITokenReq.Token } packet, err := s.transport.readPacket() if err != nil { diff --git a/vendor/golang.org/x/crypto/ssh/tcpip.go b/vendor/golang.org/x/crypto/ssh/tcpip.go index 80d35f5..ef5059a 100644 --- a/vendor/golang.org/x/crypto/ssh/tcpip.go +++ b/vendor/golang.org/x/crypto/ssh/tcpip.go @@ -5,6 +5,7 @@ package ssh import ( + "context" "errors" "fmt" "io" @@ -332,6 +333,40 @@ func (l *tcpListener) Addr() net.Addr { return l.laddr } +// DialContext initiates a connection to the addr from the remote host. +// +// The provided Context must be non-nil. If the context expires before the +// connection is complete, an error is returned. Once successfully connected, +// any expiration of the context will not affect the connection. +// +// See func Dial for additional information. +func (c *Client) DialContext(ctx context.Context, n, addr string) (net.Conn, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + type connErr struct { + conn net.Conn + err error + } + ch := make(chan connErr) + go func() { + conn, err := c.Dial(n, addr) + select { + case ch <- connErr{conn, err}: + case <-ctx.Done(): + if conn != nil { + conn.Close() + } + } + }() + select { + case res := <-ch: + return res.conn, res.err + case <-ctx.Done(): + return nil, ctx.Err() + } +} + // Dial initiates a connection to the addr from the remote host. // The resulting connection has a zero LocalAddr() and RemoteAddr(). func (c *Client) Dial(n, addr string) (net.Conn, error) { diff --git a/vendor/golang.org/x/crypto/ssh/transport.go b/vendor/golang.org/x/crypto/ssh/transport.go index da01580..0424d2d 100644 --- a/vendor/golang.org/x/crypto/ssh/transport.go +++ b/vendor/golang.org/x/crypto/ssh/transport.go @@ -49,6 +49,9 @@ type transport struct { rand io.Reader isClient bool io.Closer + + strictMode bool + initialKEXDone bool } // packetCipher represents a combination of SSH encryption/MAC @@ -74,6 +77,18 @@ type connectionState struct { pendingKeyChange chan packetCipher } +func (t *transport) setStrictMode() error { + if t.reader.seqNum != 1 { + return errors.New("ssh: sequence number != 1 when strict KEX mode requested") + } + t.strictMode = true + return nil +} + +func (t *transport) setInitialKEXDone() { + t.initialKEXDone = true +} + // prepareKeyChange sets up key material for a keychange. The key changes in // both directions are triggered by reading and writing a msgNewKey packet // respectively. @@ -112,11 +127,12 @@ func (t *transport) printPacket(p []byte, write bool) { // Read and decrypt next packet. func (t *transport) readPacket() (p []byte, err error) { for { - p, err = t.reader.readPacket(t.bufReader) + p, err = t.reader.readPacket(t.bufReader, t.strictMode) if err != nil { break } - if len(p) == 0 || (p[0] != msgIgnore && p[0] != msgDebug) { + // in strict mode we pass through DEBUG and IGNORE packets only during the initial KEX + if len(p) == 0 || (t.strictMode && !t.initialKEXDone) || (p[0] != msgIgnore && p[0] != msgDebug) { break } } @@ -127,7 +143,7 @@ func (t *transport) readPacket() (p []byte, err error) { return p, err } -func (s *connectionState) readPacket(r *bufio.Reader) ([]byte, error) { +func (s *connectionState) readPacket(r *bufio.Reader, strictMode bool) ([]byte, error) { packet, err := s.packetCipher.readCipherPacket(s.seqNum, r) s.seqNum++ if err == nil && len(packet) == 0 { @@ -140,6 +156,9 @@ func (s *connectionState) readPacket(r *bufio.Reader) ([]byte, error) { select { case cipher := <-s.pendingKeyChange: s.packetCipher = cipher + if strictMode { + s.seqNum = 0 + } default: return nil, errors.New("ssh: got bogus newkeys message") } @@ -170,10 +189,10 @@ func (t *transport) writePacket(packet []byte) error { if debugTransport { t.printPacket(packet, true) } - return t.writer.writePacket(t.bufWriter, t.rand, packet) + return t.writer.writePacket(t.bufWriter, t.rand, packet, t.strictMode) } -func (s *connectionState) writePacket(w *bufio.Writer, rand io.Reader, packet []byte) error { +func (s *connectionState) writePacket(w *bufio.Writer, rand io.Reader, packet []byte, strictMode bool) error { changeKeys := len(packet) > 0 && packet[0] == msgNewKeys err := s.packetCipher.writeCipherPacket(s.seqNum, w, rand, packet) @@ -188,6 +207,9 @@ func (s *connectionState) writePacket(w *bufio.Writer, rand io.Reader, packet [] select { case cipher := <-s.pendingKeyChange: s.packetCipher = cipher + if strictMode { + s.seqNum = 0 + } default: panic("ssh: no key material for msgNewKeys") } diff --git a/vendor/golang.org/x/exp/slices/slices.go b/vendor/golang.org/x/exp/slices/slices.go index 5e8158b..46ceac3 100644 --- a/vendor/golang.org/x/exp/slices/slices.go +++ b/vendor/golang.org/x/exp/slices/slices.go @@ -209,25 +209,37 @@ func Insert[S ~[]E, E any](s S, i int, v ...E) S { return s } -// Delete removes the elements s[i:j] from s, returning the modified slice. -// Delete panics if s[i:j] is not a valid slice of s. -// Delete is O(len(s)-j), so if many items must be deleted, it is better to -// make a single call deleting them all together than to delete one at a time. -// Delete might not modify the elements s[len(s)-(j-i):len(s)]. If those -// elements contain pointers you might consider zeroing those elements so that -// objects they reference can be garbage collected. -func Delete[S ~[]E, E any](s S, i, j int) S { - _ = s[i:j] // bounds check +// clearSlice sets all elements up to the length of s to the zero value of E. +// We may use the builtin clear func instead, and remove clearSlice, when upgrading +// to Go 1.21+. +func clearSlice[S ~[]E, E any](s S) { + var zero E + for i := range s { + s[i] = zero + } +} - return append(s[:i], s[j:]...) +// Delete removes the elements s[i:j] from s, returning the modified slice. +// Delete panics if j > len(s) or s[i:j] is not a valid slice of s. +// Delete is O(len(s)-i), so if many items must be deleted, it is better to +// make a single call deleting them all together than to delete one at a time. +// Delete zeroes the elements s[len(s)-(j-i):len(s)]. +func Delete[S ~[]E, E any](s S, i, j int) S { + _ = s[i:j:len(s)] // bounds check + + if i == j { + return s + } + + oldlen := len(s) + s = append(s[:i], s[j:]...) + clearSlice(s[len(s):oldlen]) // zero/nil out the obsolete elements, for GC + return s } // DeleteFunc removes any elements from s for which del returns true, // returning the modified slice. -// When DeleteFunc removes m elements, it might not modify the elements -// s[len(s)-m:len(s)]. If those elements contain pointers you might consider -// zeroing those elements so that objects they reference can be garbage -// collected. +// DeleteFunc zeroes the elements between the new length and the original length. func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { i := IndexFunc(s, del) if i == -1 { @@ -240,11 +252,13 @@ func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { i++ } } + clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC return s[:i] } // Replace replaces the elements s[i:j] by the given v, and returns the // modified slice. Replace panics if s[i:j] is not a valid slice of s. +// When len(v) < (j-i), Replace zeroes the elements between the new length and the original length. func Replace[S ~[]E, E any](s S, i, j int, v ...E) S { _ = s[i:j] // verify that i:j is a valid subslice @@ -272,6 +286,7 @@ func Replace[S ~[]E, E any](s S, i, j int, v ...E) S { if i+len(v) != j { copy(r[i+len(v):], s[j:]) } + clearSlice(s[tot:]) // zero/nil out the obsolete elements, for GC return r } @@ -345,9 +360,7 @@ func Clone[S ~[]E, E any](s S) S { // This is like the uniq command found on Unix. // Compact modifies the contents of the slice s and returns the modified slice, // which may have a smaller length. -// When Compact discards m elements in total, it might not modify the elements -// s[len(s)-m:len(s)]. If those elements contain pointers you might consider -// zeroing those elements so that objects they reference can be garbage collected. +// Compact zeroes the elements between the new length and the original length. func Compact[S ~[]E, E comparable](s S) S { if len(s) < 2 { return s @@ -361,11 +374,13 @@ func Compact[S ~[]E, E comparable](s S) S { i++ } } + clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC return s[:i] } // CompactFunc is like [Compact] but uses an equality function to compare elements. // For runs of elements that compare equal, CompactFunc keeps the first one. +// CompactFunc zeroes the elements between the new length and the original length. func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S { if len(s) < 2 { return s @@ -379,6 +394,7 @@ func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S { i++ } } + clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC return s[:i] } diff --git a/vendor/golang.org/x/net/html/token.go b/vendor/golang.org/x/net/html/token.go index de67f93..3c57880 100644 --- a/vendor/golang.org/x/net/html/token.go +++ b/vendor/golang.org/x/net/html/token.go @@ -910,9 +910,6 @@ func (z *Tokenizer) readTagAttrKey() { return } switch c { - case ' ', '\n', '\r', '\t', '\f', '/': - z.pendingAttr[0].end = z.raw.end - 1 - return case '=': if z.pendingAttr[0].start+1 == z.raw.end { // WHATWG 13.2.5.32, if we see an equals sign before the attribute name @@ -920,7 +917,9 @@ func (z *Tokenizer) readTagAttrKey() { continue } fallthrough - case '>': + case ' ', '\n', '\r', '\t', '\f', '/', '>': + // WHATWG 13.2.5.33 Attribute name state + // We need to reconsume the char in the after attribute name state to support the / character z.raw.end-- z.pendingAttr[0].end = z.raw.end return @@ -939,6 +938,11 @@ func (z *Tokenizer) readTagAttrVal() { if z.err != nil { return } + if c == '/' { + // WHATWG 13.2.5.34 After attribute name state + // U+002F SOLIDUS (/) - Switch to the self-closing start tag state. + return + } if c != '=' { z.raw.end-- return diff --git a/vendor/golang.org/x/sys/unix/fcntl.go b/vendor/golang.org/x/sys/unix/fcntl.go index 58c6bfc..6200876 100644 --- a/vendor/golang.org/x/sys/unix/fcntl.go +++ b/vendor/golang.org/x/sys/unix/fcntl.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build dragonfly || freebsd || linux || netbsd || openbsd +//go:build dragonfly || freebsd || linux || netbsd package unix diff --git a/vendor/golang.org/x/sys/unix/ioctl_linux.go b/vendor/golang.org/x/sys/unix/ioctl_linux.go index 0d12c08..dbe680e 100644 --- a/vendor/golang.org/x/sys/unix/ioctl_linux.go +++ b/vendor/golang.org/x/sys/unix/ioctl_linux.go @@ -231,3 +231,8 @@ func IoctlLoopGetStatus64(fd int) (*LoopInfo64, error) { func IoctlLoopSetStatus64(fd int, value *LoopInfo64) error { return ioctlPtr(fd, LOOP_SET_STATUS64, unsafe.Pointer(value)) } + +// IoctlLoopConfigure configures all loop device parameters in a single step +func IoctlLoopConfigure(fd int, value *LoopConfig) error { + return ioctlPtr(fd, LOOP_CONFIGURE, unsafe.Pointer(value)) +} diff --git a/vendor/golang.org/x/sys/unix/mkerrors.sh b/vendor/golang.org/x/sys/unix/mkerrors.sh index cbe2415..fdcaa97 100644 --- a/vendor/golang.org/x/sys/unix/mkerrors.sh +++ b/vendor/golang.org/x/sys/unix/mkerrors.sh @@ -248,6 +248,7 @@ struct ltchars { #include #include #include +#include #include #include #include @@ -283,10 +284,6 @@ struct ltchars { #include #endif -#ifndef MSG_FASTOPEN -#define MSG_FASTOPEN 0x20000000 -#endif - #ifndef PTRACE_GETREGS #define PTRACE_GETREGS 0xc #endif @@ -295,14 +292,6 @@ struct ltchars { #define PTRACE_SETREGS 0xd #endif -#ifndef SOL_NETLINK -#define SOL_NETLINK 270 -#endif - -#ifndef SOL_SMC -#define SOL_SMC 286 -#endif - #ifdef SOL_BLUETOOTH // SPARC includes this in /usr/include/sparc64-linux-gnu/bits/socket.h // but it is already in bluetooth_linux.go @@ -319,10 +308,23 @@ struct ltchars { #undef TIPC_WAIT_FOREVER #define TIPC_WAIT_FOREVER 0xffffffff -// Copied from linux/l2tp.h -// Including linux/l2tp.h here causes conflicts between linux/in.h -// and netinet/in.h included via net/route.h above. -#define IPPROTO_L2TP 115 +// Copied from linux/netfilter/nf_nat.h +// Including linux/netfilter/nf_nat.h here causes conflicts between linux/in.h +// and netinet/in.h. +#define NF_NAT_RANGE_MAP_IPS (1 << 0) +#define NF_NAT_RANGE_PROTO_SPECIFIED (1 << 1) +#define NF_NAT_RANGE_PROTO_RANDOM (1 << 2) +#define NF_NAT_RANGE_PERSISTENT (1 << 3) +#define NF_NAT_RANGE_PROTO_RANDOM_FULLY (1 << 4) +#define NF_NAT_RANGE_PROTO_OFFSET (1 << 5) +#define NF_NAT_RANGE_NETMAP (1 << 6) +#define NF_NAT_RANGE_PROTO_RANDOM_ALL \ + (NF_NAT_RANGE_PROTO_RANDOM | NF_NAT_RANGE_PROTO_RANDOM_FULLY) +#define NF_NAT_RANGE_MASK \ + (NF_NAT_RANGE_MAP_IPS | NF_NAT_RANGE_PROTO_SPECIFIED | \ + NF_NAT_RANGE_PROTO_RANDOM | NF_NAT_RANGE_PERSISTENT | \ + NF_NAT_RANGE_PROTO_RANDOM_FULLY | NF_NAT_RANGE_PROTO_OFFSET | \ + NF_NAT_RANGE_NETMAP) // Copied from linux/hid.h. // Keep in sync with the size of the referenced fields. @@ -519,6 +521,7 @@ ccflags="$@" $2 ~ /^LOCK_(SH|EX|NB|UN)$/ || $2 ~ /^LO_(KEY|NAME)_SIZE$/ || $2 ~ /^LOOP_(CLR|CTL|GET|SET)_/ || + $2 == "LOOP_CONFIGURE" || $2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|TCP|MCAST|EVFILT|NOTE|SHUT|PROT|MAP|MREMAP|MFD|T?PACKET|MSG|SCM|MCL|DT|MADV|PR|LOCAL|TCPOPT|UDP)_/ || $2 ~ /^NFC_(GENL|PROTO|COMM|RF|SE|DIRECTION|LLCP|SOCKPROTO)_/ || $2 ~ /^NFC_.*_(MAX)?SIZE$/ || @@ -560,7 +563,7 @@ ccflags="$@" $2 ~ /^RLIMIT_(AS|CORE|CPU|DATA|FSIZE|LOCKS|MEMLOCK|MSGQUEUE|NICE|NOFILE|NPROC|RSS|RTPRIO|RTTIME|SIGPENDING|STACK)|RLIM_INFINITY/ || $2 ~ /^PRIO_(PROCESS|PGRP|USER)/ || $2 ~ /^CLONE_[A-Z_]+/ || - $2 !~ /^(BPF_TIMEVAL|BPF_FIB_LOOKUP_[A-Z]+)$/ && + $2 !~ /^(BPF_TIMEVAL|BPF_FIB_LOOKUP_[A-Z]+|BPF_F_LINK)$/ && $2 ~ /^(BPF|DLT)_/ || $2 ~ /^AUDIT_/ || $2 ~ /^(CLOCK|TIMER)_/ || @@ -581,7 +584,7 @@ ccflags="$@" $2 ~ /^KEY_(SPEC|REQKEY_DEFL)_/ || $2 ~ /^KEYCTL_/ || $2 ~ /^PERF_/ || - $2 ~ /^SECCOMP_MODE_/ || + $2 ~ /^SECCOMP_/ || $2 ~ /^SEEK_/ || $2 ~ /^SCHED_/ || $2 ~ /^SPLICE_/ || @@ -602,6 +605,9 @@ ccflags="$@" $2 ~ /^FSOPT_/ || $2 ~ /^WDIO[CFS]_/ || $2 ~ /^NFN/ || + $2 !~ /^NFT_META_IIFTYPE/ && + $2 ~ /^NFT_/ || + $2 ~ /^NF_NAT_/ || $2 ~ /^XDP_/ || $2 ~ /^RWF_/ || $2 ~ /^(HDIO|WIN|SMART)_/ || diff --git a/vendor/golang.org/x/sys/unix/syscall_bsd.go b/vendor/golang.org/x/sys/unix/syscall_bsd.go index 6f328e3..a00c3e5 100644 --- a/vendor/golang.org/x/sys/unix/syscall_bsd.go +++ b/vendor/golang.org/x/sys/unix/syscall_bsd.go @@ -316,7 +316,7 @@ func GetsockoptString(fd, level, opt int) (string, error) { if err != nil { return "", err } - return string(buf[:vallen-1]), nil + return ByteSliceToString(buf[:vallen]), nil } //sys recvfrom(fd int, p []byte, flags int, from *RawSockaddrAny, fromlen *_Socklen) (n int, err error) diff --git a/vendor/golang.org/x/sys/unix/syscall_linux.go b/vendor/golang.org/x/sys/unix/syscall_linux.go index a5e1c10..0f85e29 100644 --- a/vendor/golang.org/x/sys/unix/syscall_linux.go +++ b/vendor/golang.org/x/sys/unix/syscall_linux.go @@ -61,15 +61,23 @@ func FanotifyMark(fd int, flags uint, mask uint64, dirFd int, pathname string) ( } //sys fchmodat(dirfd int, path string, mode uint32) (err error) +//sys fchmodat2(dirfd int, path string, mode uint32, flags int) (err error) -func Fchmodat(dirfd int, path string, mode uint32, flags int) (err error) { - // Linux fchmodat doesn't support the flags parameter. Mimick glibc's behavior - // and check the flags. Otherwise the mode would be applied to the symlink - // destination which is not what the user expects. - if flags&^AT_SYMLINK_NOFOLLOW != 0 { - return EINVAL - } else if flags&AT_SYMLINK_NOFOLLOW != 0 { - return EOPNOTSUPP +func Fchmodat(dirfd int, path string, mode uint32, flags int) error { + // Linux fchmodat doesn't support the flags parameter, but fchmodat2 does. + // Try fchmodat2 if flags are specified. + if flags != 0 { + err := fchmodat2(dirfd, path, mode, flags) + if err == ENOSYS { + // fchmodat2 isn't available. If the flags are known to be valid, + // return EOPNOTSUPP to indicate that fchmodat doesn't support them. + if flags&^(AT_SYMLINK_NOFOLLOW|AT_EMPTY_PATH) != 0 { + return EINVAL + } else if flags&(AT_SYMLINK_NOFOLLOW|AT_EMPTY_PATH) != 0 { + return EOPNOTSUPP + } + } + return err } return fchmodat(dirfd, path, mode) } @@ -1302,7 +1310,7 @@ func GetsockoptString(fd, level, opt int) (string, error) { return "", err } } - return string(buf[:vallen-1]), nil + return ByteSliceToString(buf[:vallen]), nil } func GetsockoptTpacketStats(fd, level, opt int) (*TpacketStats, error) { diff --git a/vendor/golang.org/x/sys/unix/syscall_openbsd.go b/vendor/golang.org/x/sys/unix/syscall_openbsd.go index d2882ee..b25343c 100644 --- a/vendor/golang.org/x/sys/unix/syscall_openbsd.go +++ b/vendor/golang.org/x/sys/unix/syscall_openbsd.go @@ -166,6 +166,20 @@ func Getresgid() (rgid, egid, sgid int) { //sys sysctl(mib []_C_int, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) = SYS___SYSCTL +//sys fcntl(fd int, cmd int, arg int) (n int, err error) +//sys fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (n int, err error) = SYS_FCNTL + +// FcntlInt performs a fcntl syscall on fd with the provided command and argument. +func FcntlInt(fd uintptr, cmd, arg int) (int, error) { + return fcntl(int(fd), cmd, arg) +} + +// FcntlFlock performs a fcntl syscall for the F_GETLK, F_SETLK or F_SETLKW command. +func FcntlFlock(fd uintptr, cmd int, lk *Flock_t) error { + _, err := fcntlPtr(int(fd), cmd, unsafe.Pointer(lk)) + return err +} + //sys ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) func Ppoll(fds []PollFd, timeout *Timespec, sigmask *Sigset_t) (n int, err error) { diff --git a/vendor/golang.org/x/sys/unix/syscall_solaris.go b/vendor/golang.org/x/sys/unix/syscall_solaris.go index 60c8142..21974af 100644 --- a/vendor/golang.org/x/sys/unix/syscall_solaris.go +++ b/vendor/golang.org/x/sys/unix/syscall_solaris.go @@ -158,7 +158,7 @@ func GetsockoptString(fd, level, opt int) (string, error) { if err != nil { return "", err } - return string(buf[:vallen-1]), nil + return ByteSliceToString(buf[:vallen]), nil } const ImplementsGetwd = true diff --git a/vendor/golang.org/x/sys/unix/syscall_zos_s390x.go b/vendor/golang.org/x/sys/unix/syscall_zos_s390x.go index d99d05f..b473038 100644 --- a/vendor/golang.org/x/sys/unix/syscall_zos_s390x.go +++ b/vendor/golang.org/x/sys/unix/syscall_zos_s390x.go @@ -1104,7 +1104,7 @@ func GetsockoptString(fd, level, opt int) (string, error) { return "", err } - return string(buf[:vallen-1]), nil + return ByteSliceToString(buf[:vallen]), nil } func Recvmsg(fd int, p, oob []byte, flags int) (n, oobn int, recvflags int, from Sockaddr, err error) { diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index 9c00cbf..36bf839 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -486,7 +486,6 @@ const ( BPF_F_ANY_ALIGNMENT = 0x2 BPF_F_BEFORE = 0x8 BPF_F_ID = 0x20 - BPF_F_LINK = 0x2000 BPF_F_NETFILTER_IP_DEFRAG = 0x1 BPF_F_QUERY_EFFECTIVE = 0x1 BPF_F_REPLACE = 0x4 @@ -1786,6 +1785,8 @@ const ( LANDLOCK_ACCESS_FS_REMOVE_FILE = 0x20 LANDLOCK_ACCESS_FS_TRUNCATE = 0x4000 LANDLOCK_ACCESS_FS_WRITE_FILE = 0x2 + LANDLOCK_ACCESS_NET_BIND_TCP = 0x1 + LANDLOCK_ACCESS_NET_CONNECT_TCP = 0x2 LANDLOCK_CREATE_RULESET_VERSION = 0x1 LINUX_REBOOT_CMD_CAD_OFF = 0x0 LINUX_REBOOT_CMD_CAD_ON = 0x89abcdef @@ -1802,6 +1803,7 @@ const ( LOCK_SH = 0x1 LOCK_UN = 0x8 LOOP_CLR_FD = 0x4c01 + LOOP_CONFIGURE = 0x4c0a LOOP_CTL_ADD = 0x4c80 LOOP_CTL_GET_FREE = 0x4c82 LOOP_CTL_REMOVE = 0x4c81 @@ -2127,6 +2129,60 @@ const ( NFNL_SUBSYS_QUEUE = 0x3 NFNL_SUBSYS_ULOG = 0x4 NFS_SUPER_MAGIC = 0x6969 + NFT_CHAIN_FLAGS = 0x7 + NFT_CHAIN_MAXNAMELEN = 0x100 + NFT_CT_MAX = 0x17 + NFT_DATA_RESERVED_MASK = 0xffffff00 + NFT_DATA_VALUE_MAXLEN = 0x40 + NFT_EXTHDR_OP_MAX = 0x4 + NFT_FIB_RESULT_MAX = 0x3 + NFT_INNER_MASK = 0xf + NFT_LOGLEVEL_MAX = 0x8 + NFT_NAME_MAXLEN = 0x100 + NFT_NG_MAX = 0x1 + NFT_OBJECT_CONNLIMIT = 0x5 + NFT_OBJECT_COUNTER = 0x1 + NFT_OBJECT_CT_EXPECT = 0x9 + NFT_OBJECT_CT_HELPER = 0x3 + NFT_OBJECT_CT_TIMEOUT = 0x7 + NFT_OBJECT_LIMIT = 0x4 + NFT_OBJECT_MAX = 0xa + NFT_OBJECT_QUOTA = 0x2 + NFT_OBJECT_SECMARK = 0x8 + NFT_OBJECT_SYNPROXY = 0xa + NFT_OBJECT_TUNNEL = 0x6 + NFT_OBJECT_UNSPEC = 0x0 + NFT_OBJ_MAXNAMELEN = 0x100 + NFT_OSF_MAXGENRELEN = 0x10 + NFT_QUEUE_FLAG_BYPASS = 0x1 + NFT_QUEUE_FLAG_CPU_FANOUT = 0x2 + NFT_QUEUE_FLAG_MASK = 0x3 + NFT_REG32_COUNT = 0x10 + NFT_REG32_SIZE = 0x4 + NFT_REG_MAX = 0x4 + NFT_REG_SIZE = 0x10 + NFT_REJECT_ICMPX_MAX = 0x3 + NFT_RT_MAX = 0x4 + NFT_SECMARK_CTX_MAXLEN = 0x100 + NFT_SET_MAXNAMELEN = 0x100 + NFT_SOCKET_MAX = 0x3 + NFT_TABLE_F_MASK = 0x3 + NFT_TABLE_MAXNAMELEN = 0x100 + NFT_TRACETYPE_MAX = 0x3 + NFT_TUNNEL_F_MASK = 0x7 + NFT_TUNNEL_MAX = 0x1 + NFT_TUNNEL_MODE_MAX = 0x2 + NFT_USERDATA_MAXLEN = 0x100 + NFT_XFRM_KEY_MAX = 0x6 + NF_NAT_RANGE_MAP_IPS = 0x1 + NF_NAT_RANGE_MASK = 0x7f + NF_NAT_RANGE_NETMAP = 0x40 + NF_NAT_RANGE_PERSISTENT = 0x8 + NF_NAT_RANGE_PROTO_OFFSET = 0x20 + NF_NAT_RANGE_PROTO_RANDOM = 0x4 + NF_NAT_RANGE_PROTO_RANDOM_ALL = 0x14 + NF_NAT_RANGE_PROTO_RANDOM_FULLY = 0x10 + NF_NAT_RANGE_PROTO_SPECIFIED = 0x2 NILFS_SUPER_MAGIC = 0x3434 NL0 = 0x0 NL1 = 0x100 @@ -2411,6 +2467,7 @@ const ( PR_MCE_KILL_GET = 0x22 PR_MCE_KILL_LATE = 0x0 PR_MCE_KILL_SET = 0x1 + PR_MDWE_NO_INHERIT = 0x2 PR_MDWE_REFUSE_EXEC_GAIN = 0x1 PR_MPX_DISABLE_MANAGEMENT = 0x2c PR_MPX_ENABLE_MANAGEMENT = 0x2b @@ -2615,8 +2672,9 @@ const ( RTAX_FEATURES = 0xc RTAX_FEATURE_ALLFRAG = 0x8 RTAX_FEATURE_ECN = 0x1 - RTAX_FEATURE_MASK = 0xf + RTAX_FEATURE_MASK = 0x1f RTAX_FEATURE_SACK = 0x2 + RTAX_FEATURE_TCP_USEC_TS = 0x10 RTAX_FEATURE_TIMESTAMP = 0x4 RTAX_HOPLIMIT = 0xa RTAX_INITCWND = 0xb @@ -2859,9 +2917,38 @@ const ( SCM_RIGHTS = 0x1 SCM_TIMESTAMP = 0x1d SC_LOG_FLUSH = 0x100000 + SECCOMP_ADDFD_FLAG_SEND = 0x2 + SECCOMP_ADDFD_FLAG_SETFD = 0x1 + SECCOMP_FILTER_FLAG_LOG = 0x2 + SECCOMP_FILTER_FLAG_NEW_LISTENER = 0x8 + SECCOMP_FILTER_FLAG_SPEC_ALLOW = 0x4 + SECCOMP_FILTER_FLAG_TSYNC = 0x1 + SECCOMP_FILTER_FLAG_TSYNC_ESRCH = 0x10 + SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV = 0x20 + SECCOMP_GET_ACTION_AVAIL = 0x2 + SECCOMP_GET_NOTIF_SIZES = 0x3 + SECCOMP_IOCTL_NOTIF_RECV = 0xc0502100 + SECCOMP_IOCTL_NOTIF_SEND = 0xc0182101 + SECCOMP_IOC_MAGIC = '!' SECCOMP_MODE_DISABLED = 0x0 SECCOMP_MODE_FILTER = 0x2 SECCOMP_MODE_STRICT = 0x1 + SECCOMP_RET_ACTION = 0x7fff0000 + SECCOMP_RET_ACTION_FULL = 0xffff0000 + SECCOMP_RET_ALLOW = 0x7fff0000 + SECCOMP_RET_DATA = 0xffff + SECCOMP_RET_ERRNO = 0x50000 + SECCOMP_RET_KILL = 0x0 + SECCOMP_RET_KILL_PROCESS = 0x80000000 + SECCOMP_RET_KILL_THREAD = 0x0 + SECCOMP_RET_LOG = 0x7ffc0000 + SECCOMP_RET_TRACE = 0x7ff00000 + SECCOMP_RET_TRAP = 0x30000 + SECCOMP_RET_USER_NOTIF = 0x7fc00000 + SECCOMP_SET_MODE_FILTER = 0x1 + SECCOMP_SET_MODE_STRICT = 0x0 + SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP = 0x1 + SECCOMP_USER_NOTIF_FLAG_CONTINUE = 0x1 SECRETMEM_MAGIC = 0x5345434d SECURITYFS_MAGIC = 0x73636673 SEEK_CUR = 0x1 @@ -3021,6 +3108,7 @@ const ( SOL_TIPC = 0x10f SOL_TLS = 0x11a SOL_UDP = 0x11 + SOL_VSOCK = 0x11f SOL_X25 = 0x106 SOL_XDP = 0x11b SOMAXCONN = 0x1000 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go index 4920821..42ff8c3 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go index a0c1e41..dca4360 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go @@ -282,6 +282,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go index c639855..5cca668 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go @@ -288,6 +288,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go index 47cc62e..d8cae6d 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go @@ -278,6 +278,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go index 27ac4a0..28e39af 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go @@ -275,6 +275,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go index 5469464..cd66e92 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x80 SIOCATMARK = 0x40047307 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go index 3adb81d..c1595eb 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x80 SIOCATMARK = 0x40047307 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go index 2dfe98f..ee9456b 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x80 SIOCATMARK = 0x40047307 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go index f5398f8..8cfca81 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x80 SIOCATMARK = 0x40047307 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go index c54f152..60b0deb 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go @@ -336,6 +336,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go index 76057dc..f90aa72 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go @@ -340,6 +340,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go index e0c3725..ba9e015 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go @@ -340,6 +340,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go index 18f2813..07cdfd6 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go @@ -272,6 +272,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go index 11619d4..2f1dd21 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go @@ -344,6 +344,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go index 396d994..f40519d 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go @@ -335,6 +335,9 @@ const ( SCM_TIMESTAMPNS = 0x21 SCM_TXTIME = 0x3f SCM_WIFI_STATUS = 0x25 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x400000 SFD_NONBLOCK = 0x4000 SF_FP = 0x38 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_linux.go b/vendor/golang.org/x/sys/unix/zsyscall_linux.go index faca7a5..1488d27 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_linux.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_linux.go @@ -37,6 +37,21 @@ func fchmodat(dirfd int, path string, mode uint32) (err error) { // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fchmodat2(dirfd int, path string, mode uint32, flags int) (err error) { + var _p0 *byte + _p0, err = BytePtrFromString(path) + if err != nil { + return + } + _, _, e1 := Syscall6(SYS_FCHMODAT2, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(mode), uintptr(flags), 0, 0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ioctl(fd int, req uint, arg uintptr) (err error) { _, _, e1 := Syscall(SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)) if e1 != 0 { diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go index 88bfc28..9dc4241 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go @@ -584,6 +584,32 @@ var libc_sysctl_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fcntl(fd int, cmd int, arg int) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_fcntl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_fcntl fcntl "libc.so" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) { r0, _, e1 := syscall_syscall6(libc_ppoll_trampoline_addr, uintptr(unsafe.Pointer(fds)), uintptr(nfds), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)), 0, 0) n = int(r0) @@ -2271,5 +2297,3 @@ func unveil(path *byte, flags *byte) (err error) { var libc_unveil_trampoline_addr uintptr //go:cgo_import_dynamic libc_unveil unveil "libc.so" - - diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.s index 4cbeff1..41b5617 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.s @@ -178,6 +178,11 @@ TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $4 DATA ·libc_sysctl_trampoline_addr(SB)/4, $libc_sysctl_trampoline<>(SB) +TEXT libc_fcntl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_fcntl(SB) +GLOBL ·libc_fcntl_trampoline_addr(SB), RODATA, $4 +DATA ·libc_fcntl_trampoline_addr(SB)/4, $libc_fcntl_trampoline<>(SB) + TEXT libc_ppoll_trampoline<>(SB),NOSPLIT,$0-0 JMP libc_ppoll(SB) GLOBL ·libc_ppoll_trampoline_addr(SB), RODATA, $4 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go index b8a67b9..0d3a075 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go @@ -584,6 +584,32 @@ var libc_sysctl_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fcntl(fd int, cmd int, arg int) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_fcntl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_fcntl fcntl "libc.so" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) { r0, _, e1 := syscall_syscall6(libc_ppoll_trampoline_addr, uintptr(unsafe.Pointer(fds)), uintptr(nfds), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)), 0, 0) n = int(r0) @@ -2271,5 +2297,3 @@ func unveil(path *byte, flags *byte) (err error) { var libc_unveil_trampoline_addr uintptr //go:cgo_import_dynamic libc_unveil unveil "libc.so" - - diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.s index 1123f27..4019a65 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.s @@ -178,6 +178,11 @@ TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) +TEXT libc_fcntl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_fcntl(SB) +GLOBL ·libc_fcntl_trampoline_addr(SB), RODATA, $8 +DATA ·libc_fcntl_trampoline_addr(SB)/8, $libc_fcntl_trampoline<>(SB) + TEXT libc_ppoll_trampoline<>(SB),NOSPLIT,$0-0 JMP libc_ppoll(SB) GLOBL ·libc_ppoll_trampoline_addr(SB), RODATA, $8 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go index af50a65..c39f777 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go @@ -584,6 +584,32 @@ var libc_sysctl_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fcntl(fd int, cmd int, arg int) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_fcntl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_fcntl fcntl "libc.so" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) { r0, _, e1 := syscall_syscall6(libc_ppoll_trampoline_addr, uintptr(unsafe.Pointer(fds)), uintptr(nfds), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)), 0, 0) n = int(r0) @@ -2271,5 +2297,3 @@ func unveil(path *byte, flags *byte) (err error) { var libc_unveil_trampoline_addr uintptr //go:cgo_import_dynamic libc_unveil unveil "libc.so" - - diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.s index 82badae..ac4af24 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.s @@ -178,6 +178,11 @@ TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $4 DATA ·libc_sysctl_trampoline_addr(SB)/4, $libc_sysctl_trampoline<>(SB) +TEXT libc_fcntl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_fcntl(SB) +GLOBL ·libc_fcntl_trampoline_addr(SB), RODATA, $4 +DATA ·libc_fcntl_trampoline_addr(SB)/4, $libc_fcntl_trampoline<>(SB) + TEXT libc_ppoll_trampoline<>(SB),NOSPLIT,$0-0 JMP libc_ppoll(SB) GLOBL ·libc_ppoll_trampoline_addr(SB), RODATA, $4 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.go index 8fb4ff3..57571d0 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.go @@ -584,6 +584,32 @@ var libc_sysctl_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fcntl(fd int, cmd int, arg int) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_fcntl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_fcntl fcntl "libc.so" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) { r0, _, e1 := syscall_syscall6(libc_ppoll_trampoline_addr, uintptr(unsafe.Pointer(fds)), uintptr(nfds), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)), 0, 0) n = int(r0) @@ -2271,5 +2297,3 @@ func unveil(path *byte, flags *byte) (err error) { var libc_unveil_trampoline_addr uintptr //go:cgo_import_dynamic libc_unveil unveil "libc.so" - - diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.s index 24d7eec..f77d532 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm64.s @@ -178,6 +178,11 @@ TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) +TEXT libc_fcntl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_fcntl(SB) +GLOBL ·libc_fcntl_trampoline_addr(SB), RODATA, $8 +DATA ·libc_fcntl_trampoline_addr(SB)/8, $libc_fcntl_trampoline<>(SB) + TEXT libc_ppoll_trampoline<>(SB),NOSPLIT,$0-0 JMP libc_ppoll(SB) GLOBL ·libc_ppoll_trampoline_addr(SB), RODATA, $8 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.go index f469a83..e62963e 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.go @@ -584,6 +584,32 @@ var libc_sysctl_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fcntl(fd int, cmd int, arg int) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_fcntl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_fcntl fcntl "libc.so" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) { r0, _, e1 := syscall_syscall6(libc_ppoll_trampoline_addr, uintptr(unsafe.Pointer(fds)), uintptr(nfds), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)), 0, 0) n = int(r0) @@ -2271,5 +2297,3 @@ func unveil(path *byte, flags *byte) (err error) { var libc_unveil_trampoline_addr uintptr //go:cgo_import_dynamic libc_unveil unveil "libc.so" - - diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s index 9a498a0..fae140b 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s @@ -178,6 +178,11 @@ TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) +TEXT libc_fcntl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_fcntl(SB) +GLOBL ·libc_fcntl_trampoline_addr(SB), RODATA, $8 +DATA ·libc_fcntl_trampoline_addr(SB)/8, $libc_fcntl_trampoline<>(SB) + TEXT libc_ppoll_trampoline<>(SB),NOSPLIT,$0-0 JMP libc_ppoll(SB) GLOBL ·libc_ppoll_trampoline_addr(SB), RODATA, $8 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.go index c26ca2e..0083135 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.go @@ -584,6 +584,32 @@ var libc_sysctl_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fcntl(fd int, cmd int, arg int) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_fcntl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_fcntl fcntl "libc.so" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) { r0, _, e1 := syscall_syscall6(libc_ppoll_trampoline_addr, uintptr(unsafe.Pointer(fds)), uintptr(nfds), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)), 0, 0) n = int(r0) @@ -2271,5 +2297,3 @@ func unveil(path *byte, flags *byte) (err error) { var libc_unveil_trampoline_addr uintptr //go:cgo_import_dynamic libc_unveil unveil "libc.so" - - diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.s index 1f224aa..9d1e0ff 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_ppc64.s @@ -213,6 +213,12 @@ TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) +TEXT libc_fcntl_trampoline<>(SB),NOSPLIT,$0-0 + CALL libc_fcntl(SB) + RET +GLOBL ·libc_fcntl_trampoline_addr(SB), RODATA, $8 +DATA ·libc_fcntl_trampoline_addr(SB)/8, $libc_fcntl_trampoline<>(SB) + TEXT libc_ppoll_trampoline<>(SB),NOSPLIT,$0-0 CALL libc_ppoll(SB) RET diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.go b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.go index bcc920d..79029ed 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.go @@ -584,6 +584,32 @@ var libc_sysctl_trampoline_addr uintptr // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func fcntl(fd int, cmd int, arg int) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +var libc_fcntl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_fcntl fcntl "libc.so" + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + +func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (n int, err error) { + r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg)) + n = int(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) { r0, _, e1 := syscall_syscall6(libc_ppoll_trampoline_addr, uintptr(unsafe.Pointer(fds)), uintptr(nfds), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)), 0, 0) n = int(r0) @@ -2271,5 +2297,3 @@ func unveil(path *byte, flags *byte) (err error) { var libc_unveil_trampoline_addr uintptr //go:cgo_import_dynamic libc_unveil unveil "libc.so" - - diff --git a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.s b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.s index 87a79c7..da115f9 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.s +++ b/vendor/golang.org/x/sys/unix/zsyscall_openbsd_riscv64.s @@ -178,6 +178,11 @@ TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) +TEXT libc_fcntl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_fcntl(SB) +GLOBL ·libc_fcntl_trampoline_addr(SB), RODATA, $8 +DATA ·libc_fcntl_trampoline_addr(SB)/8, $libc_fcntl_trampoline<>(SB) + TEXT libc_ppoll_trampoline<>(SB),NOSPLIT,$0-0 JMP libc_ppoll(SB) GLOBL ·libc_ppoll_trampoline_addr(SB), RODATA, $8 diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go index fcf3ecb..0cc3ce4 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go @@ -448,4 +448,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go index f56dc25..856d92d 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go @@ -371,4 +371,7 @@ const ( SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go index 974bf24..8d46709 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go @@ -412,4 +412,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go index 39a2739..edc1732 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go @@ -315,4 +315,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go index cf9c9d7..445eba2 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go @@ -309,4 +309,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go index 10b7362..adba01b 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go @@ -432,4 +432,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 4450 SYS_CACHESTAT = 4451 SYS_FCHMODAT2 = 4452 + SYS_MAP_SHADOW_STACK = 4453 + SYS_FUTEX_WAKE = 4454 + SYS_FUTEX_WAIT = 4455 + SYS_FUTEX_REQUEUE = 4456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go index cd4d8b4..014c4e9 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go @@ -362,4 +362,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 5450 SYS_CACHESTAT = 5451 SYS_FCHMODAT2 = 5452 + SYS_MAP_SHADOW_STACK = 5453 + SYS_FUTEX_WAKE = 5454 + SYS_FUTEX_WAIT = 5455 + SYS_FUTEX_REQUEUE = 5456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go index 2c0efca..ccc97d7 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go @@ -362,4 +362,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 5450 SYS_CACHESTAT = 5451 SYS_FCHMODAT2 = 5452 + SYS_MAP_SHADOW_STACK = 5453 + SYS_FUTEX_WAKE = 5454 + SYS_FUTEX_WAIT = 5455 + SYS_FUTEX_REQUEUE = 5456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go index a72e31d..ec2b64a 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go @@ -432,4 +432,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 4450 SYS_CACHESTAT = 4451 SYS_FCHMODAT2 = 4452 + SYS_MAP_SHADOW_STACK = 4453 + SYS_FUTEX_WAKE = 4454 + SYS_FUTEX_WAIT = 4455 + SYS_FUTEX_REQUEUE = 4456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go index c7d1e37..21a839e 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go @@ -439,4 +439,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go index f4d4838..c11121e 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go @@ -411,4 +411,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go index b64f0e5..909b631 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go @@ -411,4 +411,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go index 9571119..e49bed1 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go @@ -316,4 +316,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go index f94e943..66017d2 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go @@ -377,4 +377,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go index ba0c2bc..47bab18 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go @@ -390,4 +390,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index 997bcd5..dc0c955 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -174,7 +174,8 @@ type FscryptPolicyV2 struct { Contents_encryption_mode uint8 Filenames_encryption_mode uint8 Flags uint8 - _ [4]uint8 + Log2_data_unit_size uint8 + _ [3]uint8 Master_key_identifier [16]uint8 } @@ -455,60 +456,63 @@ type Ucred struct { } type TCPInfo struct { - State uint8 - Ca_state uint8 - Retransmits uint8 - Probes uint8 - Backoff uint8 - Options uint8 - Rto uint32 - Ato uint32 - Snd_mss uint32 - Rcv_mss uint32 - Unacked uint32 - Sacked uint32 - Lost uint32 - Retrans uint32 - Fackets uint32 - Last_data_sent uint32 - Last_ack_sent uint32 - Last_data_recv uint32 - Last_ack_recv uint32 - Pmtu uint32 - Rcv_ssthresh uint32 - Rtt uint32 - Rttvar uint32 - Snd_ssthresh uint32 - Snd_cwnd uint32 - Advmss uint32 - Reordering uint32 - Rcv_rtt uint32 - Rcv_space uint32 - Total_retrans uint32 - Pacing_rate uint64 - Max_pacing_rate uint64 - Bytes_acked uint64 - Bytes_received uint64 - Segs_out uint32 - Segs_in uint32 - Notsent_bytes uint32 - Min_rtt uint32 - Data_segs_in uint32 - Data_segs_out uint32 - Delivery_rate uint64 - Busy_time uint64 - Rwnd_limited uint64 - Sndbuf_limited uint64 - Delivered uint32 - Delivered_ce uint32 - Bytes_sent uint64 - Bytes_retrans uint64 - Dsack_dups uint32 - Reord_seen uint32 - Rcv_ooopack uint32 - Snd_wnd uint32 - Rcv_wnd uint32 - Rehash uint32 + State uint8 + Ca_state uint8 + Retransmits uint8 + Probes uint8 + Backoff uint8 + Options uint8 + Rto uint32 + Ato uint32 + Snd_mss uint32 + Rcv_mss uint32 + Unacked uint32 + Sacked uint32 + Lost uint32 + Retrans uint32 + Fackets uint32 + Last_data_sent uint32 + Last_ack_sent uint32 + Last_data_recv uint32 + Last_ack_recv uint32 + Pmtu uint32 + Rcv_ssthresh uint32 + Rtt uint32 + Rttvar uint32 + Snd_ssthresh uint32 + Snd_cwnd uint32 + Advmss uint32 + Reordering uint32 + Rcv_rtt uint32 + Rcv_space uint32 + Total_retrans uint32 + Pacing_rate uint64 + Max_pacing_rate uint64 + Bytes_acked uint64 + Bytes_received uint64 + Segs_out uint32 + Segs_in uint32 + Notsent_bytes uint32 + Min_rtt uint32 + Data_segs_in uint32 + Data_segs_out uint32 + Delivery_rate uint64 + Busy_time uint64 + Rwnd_limited uint64 + Sndbuf_limited uint64 + Delivered uint32 + Delivered_ce uint32 + Bytes_sent uint64 + Bytes_retrans uint64 + Dsack_dups uint32 + Reord_seen uint32 + Rcv_ooopack uint32 + Snd_wnd uint32 + Rcv_wnd uint32 + Rehash uint32 + Total_rto uint16 + Total_rto_recoveries uint16 + Total_rto_time uint32 } type CanFilter struct { @@ -551,7 +555,7 @@ const ( SizeofIPv6MTUInfo = 0x20 SizeofICMPv6Filter = 0x20 SizeofUcred = 0xc - SizeofTCPInfo = 0xf0 + SizeofTCPInfo = 0xf8 SizeofCanFilter = 0x8 SizeofTCPRepairOpt = 0x8 ) @@ -2671,6 +2675,7 @@ const ( BPF_PROG_TYPE_LSM = 0x1d BPF_PROG_TYPE_SK_LOOKUP = 0x1e BPF_PROG_TYPE_SYSCALL = 0x1f + BPF_PROG_TYPE_NETFILTER = 0x20 BPF_CGROUP_INET_INGRESS = 0x0 BPF_CGROUP_INET_EGRESS = 0x1 BPF_CGROUP_INET_SOCK_CREATE = 0x2 @@ -2715,6 +2720,11 @@ const ( BPF_PERF_EVENT = 0x29 BPF_TRACE_KPROBE_MULTI = 0x2a BPF_LSM_CGROUP = 0x2b + BPF_STRUCT_OPS = 0x2c + BPF_NETFILTER = 0x2d + BPF_TCX_INGRESS = 0x2e + BPF_TCX_EGRESS = 0x2f + BPF_TRACE_UPROBE_MULTI = 0x30 BPF_LINK_TYPE_UNSPEC = 0x0 BPF_LINK_TYPE_RAW_TRACEPOINT = 0x1 BPF_LINK_TYPE_TRACING = 0x2 @@ -2725,6 +2735,18 @@ const ( BPF_LINK_TYPE_PERF_EVENT = 0x7 BPF_LINK_TYPE_KPROBE_MULTI = 0x8 BPF_LINK_TYPE_STRUCT_OPS = 0x9 + BPF_LINK_TYPE_NETFILTER = 0xa + BPF_LINK_TYPE_TCX = 0xb + BPF_LINK_TYPE_UPROBE_MULTI = 0xc + BPF_PERF_EVENT_UNSPEC = 0x0 + BPF_PERF_EVENT_UPROBE = 0x1 + BPF_PERF_EVENT_URETPROBE = 0x2 + BPF_PERF_EVENT_KPROBE = 0x3 + BPF_PERF_EVENT_KRETPROBE = 0x4 + BPF_PERF_EVENT_TRACEPOINT = 0x5 + BPF_PERF_EVENT_EVENT = 0x6 + BPF_F_KPROBE_MULTI_RETURN = 0x1 + BPF_F_UPROBE_MULTI_RETURN = 0x1 BPF_ANY = 0x0 BPF_NOEXIST = 0x1 BPF_EXIST = 0x2 @@ -2742,6 +2764,8 @@ const ( BPF_F_MMAPABLE = 0x400 BPF_F_PRESERVE_ELEMS = 0x800 BPF_F_INNER_MAP = 0x1000 + BPF_F_LINK = 0x2000 + BPF_F_PATH_FD = 0x4000 BPF_STATS_RUN_TIME = 0x0 BPF_STACK_BUILD_ID_EMPTY = 0x0 BPF_STACK_BUILD_ID_VALID = 0x1 @@ -2762,6 +2786,7 @@ const ( BPF_F_ZERO_CSUM_TX = 0x2 BPF_F_DONT_FRAGMENT = 0x4 BPF_F_SEQ_NUMBER = 0x8 + BPF_F_NO_TUNNEL_KEY = 0x10 BPF_F_TUNINFO_FLAGS = 0x10 BPF_F_INDEX_MASK = 0xffffffff BPF_F_CURRENT_CPU = 0xffffffff @@ -2778,6 +2803,8 @@ const ( BPF_F_ADJ_ROOM_ENCAP_L4_UDP = 0x10 BPF_F_ADJ_ROOM_NO_CSUM_RESET = 0x20 BPF_F_ADJ_ROOM_ENCAP_L2_ETH = 0x40 + BPF_F_ADJ_ROOM_DECAP_L3_IPV4 = 0x80 + BPF_F_ADJ_ROOM_DECAP_L3_IPV6 = 0x100 BPF_ADJ_ROOM_ENCAP_L2_MASK = 0xff BPF_ADJ_ROOM_ENCAP_L2_SHIFT = 0x38 BPF_F_SYSCTL_BASE_NAME = 0x1 @@ -2866,6 +2893,8 @@ const ( BPF_DEVCG_DEV_CHAR = 0x2 BPF_FIB_LOOKUP_DIRECT = 0x1 BPF_FIB_LOOKUP_OUTPUT = 0x2 + BPF_FIB_LOOKUP_SKIP_NEIGH = 0x4 + BPF_FIB_LOOKUP_TBID = 0x8 BPF_FIB_LKUP_RET_SUCCESS = 0x0 BPF_FIB_LKUP_RET_BLACKHOLE = 0x1 BPF_FIB_LKUP_RET_UNREACHABLE = 0x2 @@ -2901,6 +2930,7 @@ const ( BPF_CORE_ENUMVAL_EXISTS = 0xa BPF_CORE_ENUMVAL_VALUE = 0xb BPF_CORE_TYPE_MATCHES = 0xc + BPF_F_TIMER_ABS = 0x1 ) const ( @@ -2979,6 +3009,12 @@ type LoopInfo64 struct { Encrypt_key [32]uint8 Init [2]uint64 } +type LoopConfig struct { + Fd uint32 + Size uint32 + Info LoopInfo64 + _ [8]uint64 +} type TIPCSocketAddr struct { Ref uint32 @@ -3367,7 +3403,7 @@ const ( DEVLINK_PORT_FN_ATTR_STATE = 0x2 DEVLINK_PORT_FN_ATTR_OPSTATE = 0x3 DEVLINK_PORT_FN_ATTR_CAPS = 0x4 - DEVLINK_PORT_FUNCTION_ATTR_MAX = 0x4 + DEVLINK_PORT_FUNCTION_ATTR_MAX = 0x5 ) type FsverityDigest struct { @@ -4151,7 +4187,8 @@ const ( ) type LandlockRulesetAttr struct { - Access_fs uint64 + Access_fs uint64 + Access_net uint64 } type LandlockPathBeneathAttr struct { @@ -5102,7 +5139,7 @@ const ( NL80211_FREQUENCY_ATTR_GO_CONCURRENT = 0xf NL80211_FREQUENCY_ATTR_INDOOR_ONLY = 0xe NL80211_FREQUENCY_ATTR_IR_CONCURRENT = 0xf - NL80211_FREQUENCY_ATTR_MAX = 0x1b + NL80211_FREQUENCY_ATTR_MAX = 0x1c NL80211_FREQUENCY_ATTR_MAX_TX_POWER = 0x6 NL80211_FREQUENCY_ATTR_NO_10MHZ = 0x11 NL80211_FREQUENCY_ATTR_NO_160MHZ = 0xc @@ -5515,7 +5552,7 @@ const ( NL80211_REGDOM_TYPE_CUSTOM_WORLD = 0x2 NL80211_REGDOM_TYPE_INTERSECTION = 0x3 NL80211_REGDOM_TYPE_WORLD = 0x1 - NL80211_REG_RULE_ATTR_MAX = 0x7 + NL80211_REG_RULE_ATTR_MAX = 0x8 NL80211_REKEY_DATA_AKM = 0x4 NL80211_REKEY_DATA_KCK = 0x2 NL80211_REKEY_DATA_KEK = 0x1 diff --git a/vendor/golang.org/x/sys/windows/env_windows.go b/vendor/golang.org/x/sys/windows/env_windows.go index b8ad192..d4577a4 100644 --- a/vendor/golang.org/x/sys/windows/env_windows.go +++ b/vendor/golang.org/x/sys/windows/env_windows.go @@ -37,14 +37,17 @@ func (token Token) Environ(inheritExisting bool) (env []string, err error) { return nil, err } defer DestroyEnvironmentBlock(block) - blockp := unsafe.Pointer(block) - for { - entry := UTF16PtrToString((*uint16)(blockp)) - if len(entry) == 0 { - break + size := unsafe.Sizeof(*block) + for *block != 0 { + // find NUL terminator + end := unsafe.Pointer(block) + for *(*uint16)(end) != 0 { + end = unsafe.Add(end, size) } - env = append(env, entry) - blockp = unsafe.Add(blockp, 2*(len(entry)+1)) + + entry := unsafe.Slice(block, (uintptr(end)-uintptr(unsafe.Pointer(block)))/size) + env = append(env, UTF16ToString(entry)) + block = (*uint16)(unsafe.Add(end, size)) } return env, nil } diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index fb6cfd0..6395a03 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -125,8 +125,7 @@ func UTF16PtrToString(p *uint16) string { for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ { ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(*p)) } - - return string(utf16.Decode(unsafe.Slice(p, n))) + return UTF16ToString(unsafe.Slice(p, n)) } func Getpagesize() int { return 4096 } @@ -155,6 +154,8 @@ func NewCallbackCDecl(fn interface{}) uintptr { //sys GetModuleFileName(module Handle, filename *uint16, size uint32) (n uint32, err error) = kernel32.GetModuleFileNameW //sys GetModuleHandleEx(flags uint32, moduleName *uint16, module *Handle) (err error) = kernel32.GetModuleHandleExW //sys SetDefaultDllDirectories(directoryFlags uint32) (err error) +//sys AddDllDirectory(path *uint16) (cookie uintptr, err error) = kernel32.AddDllDirectory +//sys RemoveDllDirectory(cookie uintptr) (err error) = kernel32.RemoveDllDirectory //sys SetDllDirectory(path string) (err error) = kernel32.SetDllDirectoryW //sys GetVersion() (ver uint32, err error) //sys FormatMessage(flags uint32, msgsrc uintptr, msgid uint32, langid uint32, buf []uint16, args *byte) (n uint32, err error) = FormatMessageW @@ -192,6 +193,7 @@ func NewCallbackCDecl(fn interface{}) uintptr { //sys GetComputerName(buf *uint16, n *uint32) (err error) = GetComputerNameW //sys GetComputerNameEx(nametype uint32, buf *uint16, n *uint32) (err error) = GetComputerNameExW //sys SetEndOfFile(handle Handle) (err error) +//sys SetFileValidData(handle Handle, validDataLength int64) (err error) //sys GetSystemTimeAsFileTime(time *Filetime) //sys GetSystemTimePreciseAsFileTime(time *Filetime) //sys GetTimeZoneInformation(tzi *Timezoneinformation) (rc uint32, err error) [failretval==0xffffffff] diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go index db6282e..e8791c8 100644 --- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go +++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go @@ -184,6 +184,7 @@ var ( procGetAdaptersInfo = modiphlpapi.NewProc("GetAdaptersInfo") procGetBestInterfaceEx = modiphlpapi.NewProc("GetBestInterfaceEx") procGetIfEntry = modiphlpapi.NewProc("GetIfEntry") + procAddDllDirectory = modkernel32.NewProc("AddDllDirectory") procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject") procCancelIo = modkernel32.NewProc("CancelIo") procCancelIoEx = modkernel32.NewProc("CancelIoEx") @@ -330,6 +331,7 @@ var ( procReadProcessMemory = modkernel32.NewProc("ReadProcessMemory") procReleaseMutex = modkernel32.NewProc("ReleaseMutex") procRemoveDirectoryW = modkernel32.NewProc("RemoveDirectoryW") + procRemoveDllDirectory = modkernel32.NewProc("RemoveDllDirectory") procResetEvent = modkernel32.NewProc("ResetEvent") procResizePseudoConsole = modkernel32.NewProc("ResizePseudoConsole") procResumeThread = modkernel32.NewProc("ResumeThread") @@ -340,6 +342,7 @@ var ( procSetDefaultDllDirectories = modkernel32.NewProc("SetDefaultDllDirectories") procSetDllDirectoryW = modkernel32.NewProc("SetDllDirectoryW") procSetEndOfFile = modkernel32.NewProc("SetEndOfFile") + procSetFileValidData = modkernel32.NewProc("SetFileValidData") procSetEnvironmentVariableW = modkernel32.NewProc("SetEnvironmentVariableW") procSetErrorMode = modkernel32.NewProc("SetErrorMode") procSetEvent = modkernel32.NewProc("SetEvent") @@ -1605,6 +1608,15 @@ func GetIfEntry(pIfRow *MibIfRow) (errcode error) { return } +func AddDllDirectory(path *uint16) (cookie uintptr, err error) { + r0, _, e1 := syscall.Syscall(procAddDllDirectory.Addr(), 1, uintptr(unsafe.Pointer(path)), 0, 0) + cookie = uintptr(r0) + if cookie == 0 { + err = errnoErr(e1) + } + return +} + func AssignProcessToJobObject(job Handle, process Handle) (err error) { r1, _, e1 := syscall.Syscall(procAssignProcessToJobObject.Addr(), 2, uintptr(job), uintptr(process), 0) if r1 == 0 { @@ -2879,6 +2891,14 @@ func RemoveDirectory(path *uint16) (err error) { return } +func RemoveDllDirectory(cookie uintptr) (err error) { + r1, _, e1 := syscall.Syscall(procRemoveDllDirectory.Addr(), 1, uintptr(cookie), 0, 0) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + func ResetEvent(event Handle) (err error) { r1, _, e1 := syscall.Syscall(procResetEvent.Addr(), 1, uintptr(event), 0, 0) if r1 == 0 { @@ -2969,6 +2989,14 @@ func SetEndOfFile(handle Handle) (err error) { return } +func SetFileValidData(handle Handle, validDataLength int64) (err error) { + r1, _, e1 := syscall.Syscall(procSetFileValidData.Addr(), 2, uintptr(handle), uintptr(validDataLength), 0) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + func SetEnvironmentVariable(name *uint16, value *uint16) (err error) { r1, _, e1 := syscall.Syscall(procSetEnvironmentVariableW.Addr(), 2, uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(value)), 0) if r1 == 0 { diff --git a/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml b/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml index 1ef386e..a656f0a 100644 --- a/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml +++ b/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml @@ -12,4 +12,8 @@ repos: rev: v1.0.0-rc.1 hooks: - id: go-imports-repo + args: + - "-local" + - "maunium.net/go/mautrix" + - "-w" - id: go-vet-repo-mod diff --git a/vendor/maunium.net/go/mautrix/CHANGELOG.md b/vendor/maunium.net/go/mautrix/CHANGELOG.md index 53dd63e..d840bcb 100644 --- a/vendor/maunium.net/go/mautrix/CHANGELOG.md +++ b/vendor/maunium.net/go/mautrix/CHANGELOG.md @@ -1,3 +1,27 @@ +## v0.17.0 (2024-01-16) + +* **Breaking change *(bridge)*** Added raw event to portal membership handling + functions. +* **Breaking change *(everything)*** Added context parameters to all functions + (started by [@recht] in [#144]). +* **Breaking change *(client)*** Moved `EventSource` to `event.Source`. +* *(client)* Removed deprecated `OldEventIgnorer`. The non-deprecated version + (`Client.DontProcessOldEvents`) is still available. +* *(crypto)* Added experimental pure Go Olm implementation to replace libolm + (thanks to [@DerLukas15] in [#106]). + * You can use the `goolm` build tag to the new implementation. +* *(bridge)* Added context parameter for bridge command events. +* *(bridge)* Added method to allow custom validation for the entire config. +* *(client)* Changed default syncer to not drop unknown events. + * The syncer will still drop known events if parsing the content fails. + * The behavior can be changed by changing the `ParseErrorHandler` function. +* *(crypto)* Fixed some places using math/rand instead of crypto/rand. + +[@DerLukas15]: https://github.com/DerLukas15 +[@recht]: https://github.com/recht +[#106]: https://github.com/mautrix/go/pull/106 +[#144]: https://github.com/mautrix/go/pull/144 + ## v0.16.2 (2023-11-16) * *(event)* Added `Redacts` field to `RedactionEventContent` for room v11+. diff --git a/vendor/maunium.net/go/mautrix/README.md b/vendor/maunium.net/go/mautrix/README.md index 04fdc0e..d45860e 100644 --- a/vendor/maunium.net/go/mautrix/README.md +++ b/vendor/maunium.net/go/mautrix/README.md @@ -17,8 +17,3 @@ In addition to the basic client API features the original project has, this fram * Structs for parsing event content * Helpers for parsing and generating Matrix HTML * Helpers for handling push rules - -This project contains modules that are licensed under Apache 2.0: - -* [maunium.net/go/mautrix/crypto/canonicaljson](crypto/canonicaljson) -* [maunium.net/go/mautrix/crypto/olm](crypto/olm) diff --git a/vendor/maunium.net/go/mautrix/client.go b/vendor/maunium.net/go/mautrix/client.go index 1772002..dfef723 100644 --- a/vendor/maunium.net/go/mautrix/client.go +++ b/vendor/maunium.net/go/mautrix/client.go @@ -27,11 +27,11 @@ import ( ) type CryptoHelper interface { - Encrypt(id.RoomID, event.Type, any) (*event.EncryptedEventContent, error) - Decrypt(*event.Event) (*event.Event, error) - WaitForSession(id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool - RequestSession(id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID) - Init() error + Encrypt(context.Context, id.RoomID, event.Type, any) (*event.EncryptedEventContent, error) + Decrypt(context.Context, *event.Event) (*event.Event, error) + WaitForSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool + RequestSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID) + Init(context.Context) error } // Deprecated: switch to zerolog @@ -100,14 +100,14 @@ type IdentityServerInfo struct { // DiscoverClientAPI resolves the client API URL from a Matrix server name. // Use ParseUserID to extract the server name from a user ID. // https://spec.matrix.org/v1.2/client-server-api/#server-discovery -func DiscoverClientAPI(serverName string) (*ClientWellKnown, error) { +func DiscoverClientAPI(ctx context.Context, serverName string) (*ClientWellKnown, error) { wellKnownURL := url.URL{ Scheme: "https", Host: serverName, Path: "/.well-known/matrix/client", } - req, err := http.NewRequest("GET", wellKnownURL.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", wellKnownURL.String(), nil) if err != nil { return nil, err } @@ -174,16 +174,25 @@ func (cli *Client) SyncWithContext(ctx context.Context) error { // We will keep syncing until the syncing state changes. Either because // Sync is called or StopSync is called. syncingID := cli.incrementSyncingID() - nextBatch := cli.Store.LoadNextBatch(cli.UserID) - filterID := cli.Store.LoadFilterID(cli.UserID) + nextBatch, err := cli.Store.LoadNextBatch(ctx, cli.UserID) + if err != nil { + return err + } + filterID, err := cli.Store.LoadFilterID(ctx, cli.UserID) + if err != nil { + return err + } + if filterID == "" { filterJSON := cli.Syncer.GetFilterJSON(cli.UserID) - resFilter, err := cli.CreateFilter(filterJSON) + resFilter, err := cli.CreateFilter(ctx, filterJSON) if err != nil { return err } filterID = resFilter.FilterID - cli.Store.SaveFilterID(cli.UserID, filterID) + if err := cli.Store.SaveFilterID(ctx, cli.UserID, filterID); err != nil { + return err + } } lastSuccessfulSync := time.Now().Add(-cli.StreamSyncMinAge - 1*time.Hour) for { @@ -192,13 +201,12 @@ func (cli *Client) SyncWithContext(ctx context.Context) error { cli.Log.Debug().Msg("Last sync is old, will stream next response") streamResp = true } - resSync, err := cli.FullSyncRequest(ReqSync{ + resSync, err := cli.FullSyncRequest(ctx, ReqSync{ Timeout: 30000, Since: nextBatch, FilterID: filterID, FullState: false, SetPresence: cli.SyncPresence, - Context: ctx, StreamResponse: streamResp, }) if err != nil { @@ -228,8 +236,11 @@ func (cli *Client) SyncWithContext(ctx context.Context) error { // Save the token now *before* processing it. This means it's possible // to not process some events, but it means that we won't get constantly stuck processing // a malformed/buggy event which keeps making us panic. - cli.Store.SaveNextBatch(cli.UserID, resSync.NextBatch) - if err = cli.Syncer.ProcessResponse(resSync, nextBatch); err != nil { + err = cli.Store.SaveNextBatch(ctx, cli.UserID, resSync.NextBatch) + if err != nil { + return err + } + if err = cli.Syncer.ProcessResponse(ctx, resSync, nextBatch); err != nil { return err } @@ -306,8 +317,8 @@ func (cli *Client) LogRequestDone(req *http.Request, resp *http.Response, err er } } -func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) { - return cli.MakeFullRequest(FullRequest{Method: method, URL: httpURL, RequestJSON: reqBody, ResponseJSON: resBody}) +func (cli *Client) MakeRequest(ctx context.Context, method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) { + return cli.MakeFullRequest(ctx, FullRequest{Method: method, URL: httpURL, RequestJSON: reqBody, ResponseJSON: resBody}) } type ClientResponseHandler = func(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) @@ -321,7 +332,6 @@ type FullRequest struct { RequestBody io.Reader RequestLength int64 ResponseJSON interface{} - Context context.Context MaxAttempts int SensitiveContent bool Handler ClientResponseHandler @@ -331,12 +341,9 @@ type FullRequest struct { var requestID int32 var logSensitiveContent = os.Getenv("MAUTRIX_LOG_SENSITIVE_CONTENT") == "yes" -func (params *FullRequest) compileRequest() (*http.Request, error) { +func (params *FullRequest) compileRequest(ctx context.Context) (*http.Request, error) { var logBody any reqBody := params.RequestBody - if params.Context == nil { - params.Context = context.Background() - } if params.RequestJSON != nil { jsonStr, err := json.Marshal(params.RequestJSON) if err != nil { @@ -363,7 +370,6 @@ func (params *FullRequest) compileRequest() (*http.Request, error) { reqBody = bytes.NewReader([]byte("{}")) } reqID := atomic.AddInt32(&requestID, 1) - ctx := params.Context logger := zerolog.Ctx(ctx) if logger.GetLevel() == zerolog.Disabled || logger == zerolog.DefaultContextLogger { logger = params.Logger @@ -398,14 +404,14 @@ func (params *FullRequest) compileRequest() (*http.Request, error) { // Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along // with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned // HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError. -func (cli *Client) MakeFullRequest(params FullRequest) ([]byte, error) { +func (cli *Client) MakeFullRequest(ctx context.Context, params FullRequest) ([]byte, error) { if params.MaxAttempts == 0 { params.MaxAttempts = 1 + cli.DefaultHTTPRetries } if params.Logger == nil { params.Logger = &cli.Log } - req, err := params.compileRequest() + req, err := params.compileRequest(ctx) if err != nil { return nil, err } @@ -567,39 +573,37 @@ func (cli *Client) executeCompiledRequest(req *http.Request, retries int, backof } // Whoami gets the user ID of the current user. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3accountwhoami -func (cli *Client) Whoami() (resp *RespWhoami, err error) { +func (cli *Client) Whoami(ctx context.Context) (resp *RespWhoami, err error) { + urlPath := cli.BuildClientURL("v3", "account", "whoami") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } // CreateFilter makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridfilter -func (cli *Client) CreateFilter(filter *Filter) (resp *RespCreateFilter, err error) { +func (cli *Client) CreateFilter(ctx context.Context, filter *Filter) (resp *RespCreateFilter, err error) { urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "filter") - _, err = cli.MakeRequest("POST", urlPath, filter, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, filter, &resp) return } // SyncRequest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync -func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence event.Presence, ctx context.Context) (resp *RespSync, err error) { - return cli.FullSyncRequest(ReqSync{ +func (cli *Client) SyncRequest(ctx context.Context, timeout int, since, filterID string, fullState bool, setPresence event.Presence) (resp *RespSync, err error) { + return cli.FullSyncRequest(ctx, ReqSync{ Timeout: timeout, Since: since, FilterID: filterID, FullState: fullState, SetPresence: setPresence, - Context: ctx, }) } type ReqSync struct { - Timeout int - Since string - FilterID string - FullState bool - SetPresence event.Presence - - Context context.Context + Timeout int + Since string + FilterID string + FullState bool + SetPresence event.Presence StreamResponse bool } @@ -623,13 +627,12 @@ func (req *ReqSync) BuildQuery() map[string]string { } // FullSyncRequest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync -func (cli *Client) FullSyncRequest(req ReqSync) (resp *RespSync, err error) { +func (cli *Client) FullSyncRequest(ctx context.Context, req ReqSync) (resp *RespSync, err error) { urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "sync"}, req.BuildQuery()) fullReq := FullRequest{ Method: http.MethodGet, URL: urlPath, ResponseJSON: &resp, - Context: req.Context, // We don't want automatic retries for SyncRequest, the Sync() wrapper handles those. MaxAttempts: 1, } @@ -637,7 +640,7 @@ func (cli *Client) FullSyncRequest(req ReqSync) (resp *RespSync, err error) { fullReq.Handler = streamResponse } start := time.Now() - _, err = cli.MakeFullRequest(fullReq) + _, err = cli.MakeFullRequest(ctx, fullReq) duration := time.Now().Sub(start) timeout := time.Duration(req.Timeout) * time.Millisecond buffer := 10 * time.Second @@ -645,7 +648,7 @@ func (cli *Client) FullSyncRequest(req ReqSync) (resp *RespSync, err error) { buffer = 1 * time.Minute } if err == nil && duration > timeout+buffer { - cli.cliOrContextLog(fullReq.Context).Warn(). + cli.cliOrContextLog(ctx).Warn(). Str("since", req.Since). Dur("duration", duration). Dur("timeout", timeout). @@ -676,18 +679,18 @@ func (cli *Client) FullSyncRequest(req ReqSync) (resp *RespSync, err error) { // } else { // // Username is available // } -func (cli *Client) RegisterAvailable(username string) (resp *RespRegisterAvailable, err error) { +func (cli *Client) RegisterAvailable(ctx context.Context, username string) (resp *RespRegisterAvailable, err error) { u := cli.BuildURLWithQuery(ClientURLPath{"v3", "register", "available"}, map[string]string{"username": username}) - _, err = cli.MakeRequest(http.MethodGet, u, nil, &resp) + _, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp) if err == nil && !resp.Available { err = fmt.Errorf(`request returned OK status without "available": true`) } return } -func (cli *Client) register(url string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { +func (cli *Client) register(ctx context.Context, url string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { var bodyBytes []byte - bodyBytes, err = cli.MakeFullRequest(FullRequest{ + bodyBytes, err = cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodPost, URL: url, RequestJSON: req, @@ -709,21 +712,21 @@ func (cli *Client) register(url string, req *ReqRegister) (resp *RespRegister, u // Register makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register // // Registers with kind=user. For kind=guest, see RegisterGuest. -func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { +func (cli *Client) Register(ctx context.Context, req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { u := cli.BuildClientURL("v3", "register") - return cli.register(u, req) + return cli.register(ctx, u, req) } // RegisterGuest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register // with kind=guest. // // For kind=user, see Register. -func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { +func (cli *Client) RegisterGuest(ctx context.Context, req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { query := map[string]string{ "kind": "guest", } u := cli.BuildURLWithQuery(ClientURLPath{"v3", "register"}, query) - return cli.register(u, req) + return cli.register(ctx, u, req) } // RegisterDummy performs m.login.dummy registration according to https://spec.matrix.org/v1.2/client-server-api/#dummy-auth @@ -741,8 +744,8 @@ func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInte // panic(err) // } // token := res.AccessToken -func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) { - res, uia, err := cli.Register(req) +func (cli *Client) RegisterDummy(ctx context.Context, req *ReqRegister) (*RespRegister, error) { + res, uia, err := cli.Register(ctx, req) if err != nil && uia == nil { return nil, err } else if uia == nil { @@ -751,7 +754,7 @@ func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) { return nil, errors.New("server does not support m.login.dummy") } req.Auth = BaseAuthData{Type: AuthTypeDummy, Session: uia.Session} - res, _, err = cli.Register(req) + res, _, err = cli.Register(ctx, req) if err != nil { return nil, err } @@ -759,15 +762,15 @@ func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) { } // GetLoginFlows fetches the login flows that the homeserver supports using https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3login -func (cli *Client) GetLoginFlows() (resp *RespLoginFlows, err error) { +func (cli *Client) GetLoginFlows(ctx context.Context) (resp *RespLoginFlows, err error) { urlPath := cli.BuildClientURL("v3", "login") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } // Login a user to the homeserver according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login -func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) { - _, err = cli.MakeFullRequest(FullRequest{ +func (cli *Client) Login(ctx context.Context, req *ReqLogin) (resp *RespLogin, err error) { + _, err = cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodPost, URL: cli.BuildClientURL("v3", "login"), RequestJSON: req, @@ -803,31 +806,31 @@ func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) { // Logout the current user. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logout // This does not clear the credentials from the client instance. See ClearCredentials() instead. -func (cli *Client) Logout() (resp *RespLogout, err error) { +func (cli *Client) Logout(ctx context.Context) (resp *RespLogout, err error) { urlPath := cli.BuildClientURL("v3", "logout") - _, err = cli.MakeRequest("POST", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, nil, &resp) return } // LogoutAll logs out all the devices of the current user. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logoutall // This does not clear the credentials from the client instance. See ClearCredentials() instead. -func (cli *Client) LogoutAll() (resp *RespLogout, err error) { +func (cli *Client) LogoutAll(ctx context.Context) (resp *RespLogout, err error) { urlPath := cli.BuildClientURL("v3", "logout", "all") - _, err = cli.MakeRequest("POST", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, nil, &resp) return } // Versions returns the list of supported Matrix versions on this homeserver. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientversions -func (cli *Client) Versions() (resp *RespVersions, err error) { +func (cli *Client) Versions(ctx context.Context) (resp *RespVersions, err error) { urlPath := cli.BuildClientURL("versions") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } // Capabilities returns capabilities on this homeserver. See https://spec.matrix.org/v1.3/client-server-api/#capabilities-negotiation -func (cli *Client) Capabilities() (resp *RespCapabilities, err error) { +func (cli *Client) Capabilities(ctx context.Context) (resp *RespCapabilities, err error) { urlPath := cli.BuildClientURL("v3", "capabilities") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } @@ -835,7 +838,7 @@ func (cli *Client) Capabilities() (resp *RespCapabilities, err error) { // // If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will // be JSON encoded and used as the request body. -func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) { +func (cli *Client) JoinRoom(ctx context.Context, roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) { var urlPath string if serverName != "" { urlPath = cli.BuildURLWithQuery(ClientURLPath{"v3", "join", roomIDorAlias}, map[string]string{ @@ -844,9 +847,12 @@ func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{ } else { urlPath = cli.BuildClientURL("v3", "join", roomIDorAlias) } - _, err = cli.MakeRequest("POST", urlPath, content, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, content, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.SetMembership(resp.RoomID, cli.UserID, event.MembershipJoin) + err = cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin) + if err != nil { + err = fmt.Errorf("failed to update state store: %w", err) + } } return } @@ -855,50 +861,53 @@ func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{ // // Unlike JoinRoom, this method can only be used to join rooms that the server already knows about. // It's mostly intended for bridges and other things where it's already certain that the server is in the room. -func (cli *Client) JoinRoomByID(roomID id.RoomID) (resp *RespJoinRoom, err error) { - _, err = cli.MakeRequest("POST", cli.BuildClientURL("v3", "rooms", roomID, "join"), nil, &resp) +func (cli *Client) JoinRoomByID(ctx context.Context, roomID id.RoomID) (resp *RespJoinRoom, err error) { + _, err = cli.MakeRequest(ctx, "POST", cli.BuildClientURL("v3", "rooms", roomID, "join"), nil, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.SetMembership(resp.RoomID, cli.UserID, event.MembershipJoin) + err = cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin) + if err != nil { + err = fmt.Errorf("failed to update state store: %w", err) + } } return } -func (cli *Client) GetProfile(mxid id.UserID) (resp *RespUserProfile, err error) { +func (cli *Client) GetProfile(ctx context.Context, mxid id.UserID) (resp *RespUserProfile, err error) { urlPath := cli.BuildClientURL("v3", "profile", mxid) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } // GetDisplayName returns the display name of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname -func (cli *Client) GetDisplayName(mxid id.UserID) (resp *RespUserDisplayName, err error) { +func (cli *Client) GetDisplayName(ctx context.Context, mxid id.UserID) (resp *RespUserDisplayName, err error) { urlPath := cli.BuildClientURL("v3", "profile", mxid, "displayname") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } // GetOwnDisplayName returns the user's display name. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname -func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) { - return cli.GetDisplayName(cli.UserID) +func (cli *Client) GetOwnDisplayName(ctx context.Context) (resp *RespUserDisplayName, err error) { + return cli.GetDisplayName(ctx, cli.UserID) } // SetDisplayName sets the user's profile display name. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3profileuseriddisplayname -func (cli *Client) SetDisplayName(displayName string) (err error) { +func (cli *Client) SetDisplayName(ctx context.Context, displayName string) (err error) { urlPath := cli.BuildClientURL("v3", "profile", cli.UserID, "displayname") s := struct { DisplayName string `json:"displayname"` }{displayName} - _, err = cli.MakeRequest("PUT", urlPath, &s, nil) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, &s, nil) return } // GetAvatarURL gets the avatar URL of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseridavatar_url -func (cli *Client) GetAvatarURL(mxid id.UserID) (url id.ContentURI, err error) { +func (cli *Client) GetAvatarURL(ctx context.Context, mxid id.UserID) (url id.ContentURI, err error) { urlPath := cli.BuildClientURL("v3", "profile", mxid, "avatar_url") s := struct { AvatarURL id.ContentURI `json:"avatar_url"` }{} - _, err = cli.MakeRequest("GET", urlPath, nil, &s) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &s) if err != nil { return } @@ -907,17 +916,17 @@ func (cli *Client) GetAvatarURL(mxid id.UserID) (url id.ContentURI, err error) { } // GetOwnAvatarURL gets the user's avatar URL. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseridavatar_url -func (cli *Client) GetOwnAvatarURL() (url id.ContentURI, err error) { - return cli.GetAvatarURL(cli.UserID) +func (cli *Client) GetOwnAvatarURL(ctx context.Context) (url id.ContentURI, err error) { + return cli.GetAvatarURL(ctx, cli.UserID) } // SetAvatarURL sets the user's avatar URL. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3profileuseridavatar_url -func (cli *Client) SetAvatarURL(url id.ContentURI) (err error) { +func (cli *Client) SetAvatarURL(ctx context.Context, url id.ContentURI) (err error) { urlPath := cli.BuildClientURL("v3", "profile", cli.UserID, "avatar_url") s := struct { AvatarURL string `json:"avatar_url"` }{url.String()} - _, err = cli.MakeRequest("PUT", urlPath, &s, nil) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, &s, nil) if err != nil { return err } @@ -926,23 +935,23 @@ func (cli *Client) SetAvatarURL(url id.ContentURI) (err error) { } // BeeperUpdateProfile sets custom fields in the user's profile. -func (cli *Client) BeeperUpdateProfile(data map[string]any) (err error) { +func (cli *Client) BeeperUpdateProfile(ctx context.Context, data map[string]any) (err error) { urlPath := cli.BuildClientURL("v3", "profile", cli.UserID) - _, err = cli.MakeRequest("PATCH", urlPath, &data, nil) + _, err = cli.MakeRequest(ctx, "PATCH", urlPath, &data, nil) return } // GetAccountData gets the user's account data of this type. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3useruseridaccount_datatype -func (cli *Client) GetAccountData(name string, output interface{}) (err error) { +func (cli *Client) GetAccountData(ctx context.Context, name string, output interface{}) (err error) { urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "account_data", name) - _, err = cli.MakeRequest("GET", urlPath, nil, output) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, output) return } // SetAccountData sets the user's account data of this type. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridaccount_datatype -func (cli *Client) SetAccountData(name string, data interface{}) (err error) { +func (cli *Client) SetAccountData(ctx context.Context, name string, data interface{}) (err error) { urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "account_data", name) - _, err = cli.MakeRequest("PUT", urlPath, data, nil) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, data, nil) if err != nil { return err } @@ -951,16 +960,16 @@ func (cli *Client) SetAccountData(name string, data interface{}) (err error) { } // GetRoomAccountData gets the user's account data of this type in a specific room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridaccount_datatype -func (cli *Client) GetRoomAccountData(roomID id.RoomID, name string, output interface{}) (err error) { +func (cli *Client) GetRoomAccountData(ctx context.Context, roomID id.RoomID, name string, output interface{}) (err error) { urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "account_data", name) - _, err = cli.MakeRequest("GET", urlPath, nil, output) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, output) return } // SetRoomAccountData sets the user's account data of this type in a specific room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridroomsroomidaccount_datatype -func (cli *Client) SetRoomAccountData(roomID id.RoomID, name string, data interface{}) (err error) { +func (cli *Client) SetRoomAccountData(ctx context.Context, roomID id.RoomID, name string, data interface{}) (err error) { urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "account_data", name) - _, err = cli.MakeRequest("PUT", urlPath, data, nil) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, data, nil) if err != nil { return err } @@ -979,7 +988,7 @@ type ReqSendEvent struct { // SendMessageEvent sends a message event into a room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. -func (cli *Client) SendMessageEvent(roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...ReqSendEvent) (resp *RespSendEvent, err error) { +func (cli *Client) SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...ReqSendEvent) (resp *RespSendEvent, err error) { var req ReqSendEvent if len(extra) > 0 { req = extra[0] @@ -1000,49 +1009,56 @@ func (cli *Client) SendMessageEvent(roomID id.RoomID, eventType event.Type, cont queryParams["fi.mau.event_id"] = req.MeowEventID.String() } - if !req.DontEncrypt && cli.Crypto != nil && eventType != event.EventReaction && eventType != event.EventEncrypted && cli.StateStore.IsEncrypted(roomID) { - contentJSON, err = cli.Crypto.Encrypt(roomID, eventType, contentJSON) + if !req.DontEncrypt && cli.Crypto != nil && eventType != event.EventReaction && eventType != event.EventEncrypted { + var isEncrypted bool + isEncrypted, err = cli.StateStore.IsEncrypted(ctx, roomID) if err != nil { - err = fmt.Errorf("failed to encrypt event: %w", err) + err = fmt.Errorf("failed to check if room is encrypted: %w", err) return } - eventType = event.EventEncrypted + if isEncrypted { + if contentJSON, err = cli.Crypto.Encrypt(ctx, roomID, eventType, contentJSON); err != nil { + err = fmt.Errorf("failed to encrypt event: %w", err) + return + } + eventType = event.EventEncrypted + } } urlData := ClientURLPath{"v3", "rooms", roomID, "send", eventType.String(), txnID} urlPath := cli.BuildURLWithQuery(urlData, queryParams) - _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, contentJSON, &resp) return } // SendStateEvent sends a state event into a room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. -func (cli *Client) SendStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { +func (cli *Client) SendStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) { urlPath := cli.BuildClientURL("v3", "rooms", roomID, "state", eventType.String(), stateKey) - _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, contentJSON, &resp) if err == nil && cli.StateStore != nil { - cli.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, contentJSON) + cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, contentJSON) } return } // SendMassagedStateEvent sends a state event into a room with a custom timestamp. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. -func (cli *Client) SendMassagedStateEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { +func (cli *Client) SendMassagedStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) { urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{ "ts": strconv.FormatInt(ts, 10), }) - _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, contentJSON, &resp) if err == nil && cli.StateStore != nil { - cli.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, contentJSON) + cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, contentJSON) } return } // SendText sends an m.room.message event into the given room with a msgtype of m.text // See https://spec.matrix.org/v1.2/client-server-api/#mtext -func (cli *Client) SendText(roomID id.RoomID, text string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, event.EventMessage, &event.MessageEventContent{ +func (cli *Client) SendText(ctx context.Context, roomID id.RoomID, text string) (*RespSendEvent, error) { + return cli.SendMessageEvent(ctx, roomID, event.EventMessage, &event.MessageEventContent{ MsgType: event.MsgText, Body: text, }) @@ -1050,15 +1066,15 @@ func (cli *Client) SendText(roomID id.RoomID, text string) (*RespSendEvent, erro // SendNotice sends an m.room.message event into the given room with a msgtype of m.notice // See https://spec.matrix.org/v1.2/client-server-api/#mnotice -func (cli *Client) SendNotice(roomID id.RoomID, text string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, event.EventMessage, &event.MessageEventContent{ +func (cli *Client) SendNotice(ctx context.Context, roomID id.RoomID, text string) (*RespSendEvent, error) { + return cli.SendMessageEvent(ctx, roomID, event.EventMessage, &event.MessageEventContent{ MsgType: event.MsgNotice, Body: text, }) } -func (cli *Client) SendReaction(roomID id.RoomID, eventID id.EventID, reaction string) (*RespSendEvent, error) { - return cli.SendMessageEvent(roomID, event.EventReaction, &event.ReactionEventContent{ +func (cli *Client) SendReaction(ctx context.Context, roomID id.RoomID, eventID id.EventID, reaction string) (*RespSendEvent, error) { + return cli.SendMessageEvent(ctx, roomID, event.EventReaction, &event.ReactionEventContent{ RelatesTo: event.RelatesTo{ EventID: eventID, Type: event.RelAnnotation, @@ -1068,7 +1084,7 @@ func (cli *Client) SendReaction(roomID id.RoomID, eventID id.EventID, reaction s } // RedactEvent redacts the given event. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidredacteventidtxnid -func (cli *Client) RedactEvent(roomID id.RoomID, eventID id.EventID, extra ...ReqRedact) (resp *RespSendEvent, err error) { +func (cli *Client) RedactEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID, extra ...ReqRedact) (resp *RespSendEvent, err error) { req := ReqRedact{} if len(extra) > 0 { req = extra[0] @@ -1086,7 +1102,7 @@ func (cli *Client) RedactEvent(roomID id.RoomID, eventID id.EventID, extra ...Re txnID = cli.TxnID() } urlPath := cli.BuildClientURL("v3", "rooms", roomID, "redact", eventID, txnID) - _, err = cli.MakeRequest("PUT", urlPath, req.Extra, &resp) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, req.Extra, &resp) return } @@ -1096,30 +1112,40 @@ func (cli *Client) RedactEvent(roomID id.RoomID, eventID id.EventID, extra ...Re // Preset: "public_chat", // }) // fmt.Println("Room:", resp.RoomID) -func (cli *Client) CreateRoom(req *ReqCreateRoom) (resp *RespCreateRoom, err error) { +func (cli *Client) CreateRoom(ctx context.Context, req *ReqCreateRoom) (resp *RespCreateRoom, err error) { urlPath := cli.BuildClientURL("v3", "createRoom") - _, err = cli.MakeRequest("POST", urlPath, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.SetMembership(resp.RoomID, cli.UserID, event.MembershipJoin) + storeErr := cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin) + if storeErr != nil { + cli.cliOrContextLog(ctx).Warn().Err(storeErr). + Stringer("creator_user_id", cli.UserID). + Msg("Failed to update creator membership in state store after creating room") + } for _, evt := range req.InitialState { - UpdateStateStore(cli.StateStore, evt) + UpdateStateStore(ctx, cli.StateStore, evt) } inviteMembership := event.MembershipInvite if req.BeeperAutoJoinInvites { inviteMembership = event.MembershipJoin } for _, invitee := range req.Invite { - cli.StateStore.SetMembership(resp.RoomID, invitee, inviteMembership) + storeErr = cli.StateStore.SetMembership(ctx, resp.RoomID, invitee, inviteMembership) + if storeErr != nil { + cli.cliOrContextLog(ctx).Warn().Err(storeErr). + Stringer("invitee_user_id", invitee). + Msg("Failed to update membership in state store after creating room") + } } for _, evt := range req.InitialState { - cli.updateStoreWithOutgoingEvent(resp.RoomID, evt.Type, evt.GetStateKey(), &evt.Content) + cli.updateStoreWithOutgoingEvent(ctx, resp.RoomID, evt.Type, evt.GetStateKey(), &evt.Content) } } return } // LeaveRoom leaves the given room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidleave -func (cli *Client) LeaveRoom(roomID id.RoomID, optionalReq ...*ReqLeave) (resp *RespLeaveRoom, err error) { +func (cli *Client) LeaveRoom(ctx context.Context, roomID id.RoomID, optionalReq ...*ReqLeave) (resp *RespLeaveRoom, err error) { req := &ReqLeave{} if len(optionalReq) == 1 { req = optionalReq[0] @@ -1127,96 +1153,111 @@ func (cli *Client) LeaveRoom(roomID id.RoomID, optionalReq ...*ReqLeave) (resp * panic("invalid number of arguments to LeaveRoom") } u := cli.BuildClientURL("v3", "rooms", roomID, "leave") - _, err = cli.MakeRequest("POST", u, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", u, req, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.SetMembership(roomID, cli.UserID, event.MembershipLeave) + err = cli.StateStore.SetMembership(ctx, roomID, cli.UserID, event.MembershipLeave) + if err != nil { + err = fmt.Errorf("failed to update membership in state store: %w", err) + } } return } // ForgetRoom forgets a room entirely. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidforget -func (cli *Client) ForgetRoom(roomID id.RoomID) (resp *RespForgetRoom, err error) { +func (cli *Client) ForgetRoom(ctx context.Context, roomID id.RoomID) (resp *RespForgetRoom, err error) { u := cli.BuildClientURL("v3", "rooms", roomID, "forget") - _, err = cli.MakeRequest("POST", u, struct{}{}, &resp) + _, err = cli.MakeRequest(ctx, "POST", u, struct{}{}, &resp) return } // InviteUser invites a user to a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite -func (cli *Client) InviteUser(roomID id.RoomID, req *ReqInviteUser) (resp *RespInviteUser, err error) { +func (cli *Client) InviteUser(ctx context.Context, roomID id.RoomID, req *ReqInviteUser) (resp *RespInviteUser, err error) { u := cli.BuildClientURL("v3", "rooms", roomID, "invite") - _, err = cli.MakeRequest("POST", u, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", u, req, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.SetMembership(roomID, req.UserID, event.MembershipInvite) + err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipInvite) + if err != nil { + err = fmt.Errorf("failed to update membership in state store: %w", err) + } } return } // InviteUserByThirdParty invites a third-party identifier to a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite-1 -func (cli *Client) InviteUserByThirdParty(roomID id.RoomID, req *ReqInvite3PID) (resp *RespInviteUser, err error) { +func (cli *Client) InviteUserByThirdParty(ctx context.Context, roomID id.RoomID, req *ReqInvite3PID) (resp *RespInviteUser, err error) { u := cli.BuildClientURL("v3", "rooms", roomID, "invite") - _, err = cli.MakeRequest("POST", u, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", u, req, &resp) return } // KickUser kicks a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidkick -func (cli *Client) KickUser(roomID id.RoomID, req *ReqKickUser) (resp *RespKickUser, err error) { +func (cli *Client) KickUser(ctx context.Context, roomID id.RoomID, req *ReqKickUser) (resp *RespKickUser, err error) { u := cli.BuildClientURL("v3", "rooms", roomID, "kick") - _, err = cli.MakeRequest("POST", u, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", u, req, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.SetMembership(roomID, req.UserID, event.MembershipLeave) + err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipLeave) + if err != nil { + err = fmt.Errorf("failed to update membership in state store: %w", err) + } } return } // BanUser bans a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban -func (cli *Client) BanUser(roomID id.RoomID, req *ReqBanUser) (resp *RespBanUser, err error) { +func (cli *Client) BanUser(ctx context.Context, roomID id.RoomID, req *ReqBanUser) (resp *RespBanUser, err error) { u := cli.BuildClientURL("v3", "rooms", roomID, "ban") - _, err = cli.MakeRequest("POST", u, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", u, req, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.SetMembership(roomID, req.UserID, event.MembershipBan) + err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipBan) + if err != nil { + err = fmt.Errorf("failed to update membership in state store: %w", err) + } } return } // UnbanUser unbans a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban -func (cli *Client) UnbanUser(roomID id.RoomID, req *ReqUnbanUser) (resp *RespUnbanUser, err error) { +func (cli *Client) UnbanUser(ctx context.Context, roomID id.RoomID, req *ReqUnbanUser) (resp *RespUnbanUser, err error) { u := cli.BuildClientURL("v3", "rooms", roomID, "unban") - _, err = cli.MakeRequest("POST", u, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", u, req, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.SetMembership(roomID, req.UserID, event.MembershipLeave) + err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipLeave) + if err != nil { + err = fmt.Errorf("failed to update membership in state store: %w", err) + } } return } // UserTyping sets the typing status of the user. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidtypinguserid -func (cli *Client) UserTyping(roomID id.RoomID, typing bool, timeout time.Duration) (resp *RespTyping, err error) { +func (cli *Client) UserTyping(ctx context.Context, roomID id.RoomID, typing bool, timeout time.Duration) (resp *RespTyping, err error) { req := ReqTyping{Typing: typing, Timeout: timeout.Milliseconds()} u := cli.BuildClientURL("v3", "rooms", roomID, "typing", cli.UserID) - _, err = cli.MakeRequest("PUT", u, req, &resp) + _, err = cli.MakeRequest(ctx, "PUT", u, req, &resp) return } // GetPresence gets the presence of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus -func (cli *Client) GetPresence(userID id.UserID) (resp *RespPresence, err error) { +func (cli *Client) GetPresence(ctx context.Context, userID id.UserID) (resp *RespPresence, err error) { resp = new(RespPresence) u := cli.BuildClientURL("v3", "presence", userID, "status") - _, err = cli.MakeRequest("GET", u, nil, resp) + _, err = cli.MakeRequest(ctx, "GET", u, nil, resp) return } // GetOwnPresence gets the user's presence. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus -func (cli *Client) GetOwnPresence() (resp *RespPresence, err error) { - return cli.GetPresence(cli.UserID) +func (cli *Client) GetOwnPresence(ctx context.Context) (resp *RespPresence, err error) { + return cli.GetPresence(ctx, cli.UserID) } -func (cli *Client) SetPresence(status event.Presence) (err error) { +func (cli *Client) SetPresence(ctx context.Context, status event.Presence) (err error) { req := ReqPresence{Presence: status} u := cli.BuildClientURL("v3", "presence", cli.UserID, "status") - _, err = cli.MakeRequest("PUT", u, req, nil) + _, err = cli.MakeRequest(ctx, "PUT", u, req, nil) return } -func (cli *Client) updateStoreWithOutgoingEvent(roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) { +func (cli *Client) updateStoreWithOutgoingEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) { if cli.StateStore == nil { return } @@ -1246,17 +1287,17 @@ func (cli *Client) updateStoreWithOutgoingEvent(roomID id.RoomID, eventType even } return } - UpdateStateStore(cli.StateStore, fakeEvt) + UpdateStateStore(ctx, cli.StateStore, fakeEvt) } // StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with // the HTTP response body, or return an error. // See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidstateeventtypestatekey -func (cli *Client) StateEvent(roomID id.RoomID, eventType event.Type, stateKey string, outContent interface{}) (err error) { +func (cli *Client) StateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, outContent interface{}) (err error) { u := cli.BuildClientURL("v3", "rooms", roomID, "state", eventType.String(), stateKey) - _, err = cli.MakeRequest("GET", u, nil, outContent) + _, err = cli.MakeRequest(ctx, "GET", u, nil, outContent) if err == nil && cli.StateStore != nil { - cli.updateStoreWithOutgoingEvent(roomID, eventType, stateKey, outContent) + cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, outContent) } return } @@ -1302,18 +1343,21 @@ func parseRoomStateArray(_ *http.Request, res *http.Response, responseJSON inter // State gets all state in a room. // See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidstate -func (cli *Client) State(roomID id.RoomID) (stateMap RoomStateMap, err error) { - _, err = cli.MakeFullRequest(FullRequest{ +func (cli *Client) State(ctx context.Context, roomID id.RoomID) (stateMap RoomStateMap, err error) { + _, err = cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodGet, URL: cli.BuildClientURL("v3", "rooms", roomID, "state"), ResponseJSON: &stateMap, Handler: parseRoomStateArray, }) if err == nil && cli.StateStore != nil { - cli.StateStore.ClearCachedMembers(roomID) + clearErr := cli.StateStore.ClearCachedMembers(ctx, roomID) + cli.cliOrContextLog(ctx).Warn().Err(clearErr). + Stringer("room_id", roomID). + Msg("Failed to clear cached member list after fetching state") for _, evts := range stateMap { for _, evt := range evts { - UpdateStateStore(cli.StateStore, evt) + UpdateStateStore(ctx, cli.StateStore, evt) } } } @@ -1321,34 +1365,35 @@ func (cli *Client) State(roomID id.RoomID) (stateMap RoomStateMap, err error) { } // GetMediaConfig fetches the configuration of the content repository, such as upload limitations. -func (cli *Client) GetMediaConfig() (resp *RespMediaConfig, err error) { +func (cli *Client) GetMediaConfig(ctx context.Context) (resp *RespMediaConfig, err error) { u := cli.BuildURL(MediaURLPath{"v3", "config"}) - _, err = cli.MakeRequest("GET", u, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", u, nil, &resp) return } // UploadLink uploads an HTTP URL and then returns an MXC URI. -func (cli *Client) UploadLink(link string) (*RespMediaUpload, error) { - res, err := cli.Client.Get(link) +func (cli *Client) UploadLink(ctx context.Context, link string) (*RespMediaUpload, error) { + req, err := http.NewRequestWithContext(ctx, "GET", link, nil) + if err != nil { + return nil, err + } + + res, err := cli.Client.Do(req) if res != nil { defer res.Body.Close() } if err != nil { return nil, err } - return cli.Upload(res.Body, res.Header.Get("Content-Type"), res.ContentLength) + return cli.Upload(ctx, res.Body, res.Header.Get("Content-Type"), res.ContentLength) } func (cli *Client) GetDownloadURL(mxcURL id.ContentURI) string { return cli.BuildURLWithQuery(MediaURLPath{"v3", "download", mxcURL.Homeserver, mxcURL.FileID}, map[string]string{"allow_redirect": "true"}) } -func (cli *Client) Download(mxcURL id.ContentURI) (io.ReadCloser, error) { - return cli.DownloadContext(context.Background(), mxcURL) -} - -func (cli *Client) DownloadContext(ctx context.Context, mxcURL id.ContentURI) (io.ReadCloser, error) { - resp, err := cli.downloadContext(ctx, mxcURL) +func (cli *Client) Download(ctx context.Context, mxcURL id.ContentURI) (io.ReadCloser, error) { + resp, err := cli.download(ctx, mxcURL) if err != nil { return nil, err } @@ -1411,7 +1456,7 @@ func (cli *Client) doMediaRequest(req *http.Request, retries int, backoff time.D return res, err } -func (cli *Client) downloadContext(ctx context.Context, mxcURL id.ContentURI) (*http.Response, error) { +func (cli *Client) download(ctx context.Context, mxcURL id.ContentURI) (*http.Response, error) { ctxLog := zerolog.Ctx(ctx) if ctxLog.GetLevel() == zerolog.Disabled || ctxLog == zerolog.DefaultContextLogger { ctx = cli.Log.WithContext(ctx) @@ -1424,12 +1469,8 @@ func (cli *Client) downloadContext(ctx context.Context, mxcURL id.ContentURI) (* return cli.doMediaRequest(req, cli.DefaultHTTPRetries, 4*time.Second) } -func (cli *Client) DownloadBytes(mxcURL id.ContentURI) ([]byte, error) { - return cli.DownloadBytesContext(context.Background(), mxcURL) -} - -func (cli *Client) DownloadBytesContext(ctx context.Context, mxcURL id.ContentURI) ([]byte, error) { - resp, err := cli.downloadContext(ctx, mxcURL) +func (cli *Client) DownloadBytes(ctx context.Context, mxcURL id.ContentURI) ([]byte, error) { + resp, err := cli.download(ctx, mxcURL) if err != nil { return nil, err } @@ -1440,10 +1481,10 @@ func (cli *Client) DownloadBytesContext(ctx context.Context, mxcURL id.ContentUR // CreateMXC creates a blank Matrix content URI to allow uploading the content asynchronously later. // // See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create -func (cli *Client) CreateMXC() (*RespCreateMXC, error) { +func (cli *Client) CreateMXC(ctx context.Context) (*RespCreateMXC, error) { u, _ := url.Parse(cli.BuildURL(MediaURLPath{"v1", "create"})) var m RespCreateMXC - _, err := cli.MakeFullRequest(FullRequest{ + _, err := cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodPost, URL: u.String(), ResponseJSON: &m, @@ -1456,15 +1497,15 @@ func (cli *Client) CreateMXC() (*RespCreateMXC, error) { // // See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create // and https://spec.matrix.org/v1.7/client-server-api/#put_matrixmediav3uploadservernamemediaid -func (cli *Client) UploadAsync(req ReqUploadMedia) (*RespCreateMXC, error) { - resp, err := cli.CreateMXC() +func (cli *Client) UploadAsync(ctx context.Context, req ReqUploadMedia) (*RespCreateMXC, error) { + resp, err := cli.CreateMXC(ctx) if err != nil { return nil, err } req.MXC = resp.ContentURI req.UnstableUploadURL = resp.UnstableUploadURL go func() { - _, err = cli.UploadMedia(req) + _, err = cli.UploadMedia(ctx, req) if err != nil { cli.Log.Error().Str("mxc", req.MXC.String()).Err(err).Msg("Async upload of media failed") } @@ -1472,12 +1513,12 @@ func (cli *Client) UploadAsync(req ReqUploadMedia) (*RespCreateMXC, error) { return resp, nil } -func (cli *Client) UploadBytes(data []byte, contentType string) (*RespMediaUpload, error) { - return cli.UploadBytesWithName(data, contentType, "") +func (cli *Client) UploadBytes(ctx context.Context, data []byte, contentType string) (*RespMediaUpload, error) { + return cli.UploadBytesWithName(ctx, data, contentType, "") } -func (cli *Client) UploadBytesWithName(data []byte, contentType, fileName string) (*RespMediaUpload, error) { - return cli.UploadMedia(ReqUploadMedia{ +func (cli *Client) UploadBytesWithName(ctx context.Context, data []byte, contentType, fileName string) (*RespMediaUpload, error) { + return cli.UploadMedia(ctx, ReqUploadMedia{ ContentBytes: data, ContentType: contentType, FileName: fileName, @@ -1487,8 +1528,8 @@ func (cli *Client) UploadBytesWithName(data []byte, contentType, fileName string // Upload uploads the given data to the content repository and returns an MXC URI. // // Deprecated: UploadMedia should be used instead. -func (cli *Client) Upload(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) { - return cli.UploadMedia(ReqUploadMedia{ +func (cli *Client) Upload(ctx context.Context, content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) { + return cli.UploadMedia(ctx, ReqUploadMedia{ Content: content, ContentLength: contentLength, ContentType: contentType, @@ -1511,9 +1552,9 @@ type ReqUploadMedia struct { UnstableUploadURL string } -func (cli *Client) tryUploadMediaToURL(url, contentType string, content io.Reader) (*http.Response, error) { +func (cli *Client) tryUploadMediaToURL(ctx context.Context, url, contentType string, content io.Reader) (*http.Response, error) { cli.Log.Debug().Str("url", url).Msg("Uploading media to external URL") - req, err := http.NewRequest(http.MethodPut, url, content) + req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, content) if err != nil { return nil, err } @@ -1523,7 +1564,7 @@ func (cli *Client) tryUploadMediaToURL(url, contentType string, content io.Reade return http.DefaultClient.Do(req) } -func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, error) { +func (cli *Client) uploadMediaToURL(ctx context.Context, data ReqUploadMedia) (*RespMediaUpload, error) { retries := cli.DefaultHTTPRetries if data.ContentBytes == nil { // Can't retry with a reader @@ -1536,7 +1577,7 @@ func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, erro } else { data.Content = nil } - resp, err := cli.tryUploadMediaToURL(data.UnstableUploadURL, data.ContentType, reader) + resp, err := cli.tryUploadMediaToURL(ctx, data.UnstableUploadURL, data.ContentType, reader) if err == nil { if resp.StatusCode >= 200 && resp.StatusCode < 300 { // Everything is fine @@ -1562,7 +1603,7 @@ func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, erro notifyURL := cli.BuildURLWithQuery(MediaURLPath{"unstable", "com.beeper.msc3870", "upload", data.MXC.Homeserver, data.MXC.FileID, "complete"}, query) var m *RespMediaUpload - _, err := cli.MakeFullRequest(FullRequest{ + _, err := cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodPost, URL: notifyURL, ResponseJSON: m, @@ -1576,12 +1617,12 @@ func (cli *Client) uploadMediaToURL(data ReqUploadMedia) (*RespMediaUpload, erro // UploadMedia uploads the given data to the content repository and returns an MXC URI. // See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav3upload -func (cli *Client) UploadMedia(data ReqUploadMedia) (*RespMediaUpload, error) { +func (cli *Client) UploadMedia(ctx context.Context, data ReqUploadMedia) (*RespMediaUpload, error) { if data.UnstableUploadURL != "" { if data.MXC.IsEmpty() { return nil, errors.New("MXC must also be set when uploading to external URL") } - return cli.uploadMediaToURL(data) + return cli.uploadMediaToURL(ctx, data) } u, _ := url.Parse(cli.BuildURL(MediaURLPath{"v3", "upload"})) method := http.MethodPost @@ -1601,7 +1642,7 @@ func (cli *Client) UploadMedia(data ReqUploadMedia) (*RespMediaUpload, error) { } var m RespMediaUpload - _, err := cli.MakeFullRequest(FullRequest{ + _, err := cli.MakeFullRequest(ctx, FullRequest{ Method: method, URL: u.String(), Headers: headers, @@ -1616,12 +1657,12 @@ func (cli *Client) UploadMedia(data ReqUploadMedia) (*RespMediaUpload, error) { // GetURLPreview asks the homeserver to fetch a preview for a given URL. // // See https://spec.matrix.org/v1.2/client-server-api/#get_matrixmediav3preview_url -func (cli *Client) GetURLPreview(url string) (*RespPreviewURL, error) { +func (cli *Client) GetURLPreview(ctx context.Context, url string) (*RespPreviewURL, error) { reqURL := cli.BuildURLWithQuery(MediaURLPath{"v3", "preview_url"}, map[string]string{ "url": url, }) var output RespPreviewURL - _, err := cli.MakeRequest(http.MethodGet, reqURL, nil, &output) + _, err := cli.MakeRequest(ctx, http.MethodGet, reqURL, nil, &output) return &output, err } @@ -1629,23 +1670,32 @@ func (cli *Client) GetURLPreview(url string) (*RespPreviewURL, error) { // // In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes. // This API is primarily designed for application services which may want to efficiently look up joined members in a room. -func (cli *Client) JoinedMembers(roomID id.RoomID) (resp *RespJoinedMembers, err error) { +func (cli *Client) JoinedMembers(ctx context.Context, roomID id.RoomID) (resp *RespJoinedMembers, err error) { u := cli.BuildClientURL("v3", "rooms", roomID, "joined_members") - _, err = cli.MakeRequest("GET", u, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", u, nil, &resp) if err == nil && cli.StateStore != nil { - cli.StateStore.ClearCachedMembers(roomID, event.MembershipJoin) + clearErr := cli.StateStore.ClearCachedMembers(ctx, roomID, event.MembershipJoin) + cli.cliOrContextLog(ctx).Warn().Err(clearErr). + Stringer("room_id", roomID). + Msg("Failed to clear cached member list after fetching joined members") for userID, member := range resp.Joined { - cli.StateStore.SetMember(roomID, userID, &event.MemberEventContent{ + updateErr := cli.StateStore.SetMember(ctx, roomID, userID, &event.MemberEventContent{ Membership: event.MembershipJoin, AvatarURL: id.ContentURIString(member.AvatarURL), Displayname: member.DisplayName, }) + if updateErr != nil { + cli.cliOrContextLog(ctx).Warn().Err(clearErr). + Stringer("room_id", roomID). + Stringer("user_id", userID). + Msg("Failed to update membership in state store after fetching joined members") + } } } return } -func (cli *Client) Members(roomID id.RoomID, req ...ReqMembers) (resp *RespMembers, err error) { +func (cli *Client) Members(ctx context.Context, roomID id.RoomID, req ...ReqMembers) (resp *RespMembers, err error) { var extra ReqMembers if len(req) > 0 { extra = req[0] @@ -1661,17 +1711,20 @@ func (cli *Client) Members(roomID id.RoomID, req ...ReqMembers) (resp *RespMembe query["not_membership"] = string(extra.NotMembership) } u := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "members"}, query) - _, err = cli.MakeRequest("GET", u, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", u, nil, &resp) if err == nil && cli.StateStore != nil { var clearMemberships []event.Membership if extra.Membership != "" { clearMemberships = append(clearMemberships, extra.Membership) } if extra.NotMembership == "" { - cli.StateStore.ClearCachedMembers(roomID, clearMemberships...) + clearErr := cli.StateStore.ClearCachedMembers(ctx, roomID, clearMemberships...) + cli.cliOrContextLog(ctx).Warn().Err(clearErr). + Stringer("room_id", roomID). + Msg("Failed to clear cached member list after fetching joined members") } for _, evt := range resp.Chunk { - UpdateStateStore(cli.StateStore, evt) + UpdateStateStore(ctx, cli.StateStore, evt) } } return @@ -1681,9 +1734,9 @@ func (cli *Client) Members(roomID id.RoomID, req ...ReqMembers) (resp *RespMembe // // In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes. // This API is primarily designed for application services which may want to efficiently look up joined rooms. -func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) { +func (cli *Client) JoinedRooms(ctx context.Context) (resp *RespJoinedRooms, err error) { u := cli.BuildClientURL("v3", "joined_rooms") - _, err = cli.MakeRequest("GET", u, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", u, nil, &resp) return } @@ -1693,16 +1746,16 @@ func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) { // when it encounters another space as a child it recurses into that space before returning non-space children. // // The second function parameter specifies query parameters to limit the response. No query parameters will be added if it's nil. -func (cli *Client) Hierarchy(roomID id.RoomID, req *ReqHierarchy) (resp *RespHierarchy, err error) { +func (cli *Client) Hierarchy(ctx context.Context, roomID id.RoomID, req *ReqHierarchy) (resp *RespHierarchy, err error) { urlPath := cli.BuildURLWithQuery(ClientURLPath{"v1", "rooms", roomID, "hierarchy"}, req.Query()) - _, err = cli.MakeRequest(http.MethodGet, urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp) return } // Messages returns a list of message and state events for a room. It uses // pagination query parameters to paginate history in the room. // See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidmessages -func (cli *Client) Messages(roomID id.RoomID, from, to string, dir Direction, filter *FilterPart, limit int) (resp *RespMessages, err error) { +func (cli *Client) Messages(ctx context.Context, roomID id.RoomID, from, to string, dir Direction, filter *FilterPart, limit int) (resp *RespMessages, err error) { query := map[string]string{ "from": from, "dir": string(dir), @@ -1722,20 +1775,20 @@ func (cli *Client) Messages(roomID id.RoomID, from, to string, dir Direction, fi } urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "messages"}, query) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } // TimestampToEvent finds the ID of the event closest to the given timestamp. // // See https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv1roomsroomidtimestamp_to_event -func (cli *Client) TimestampToEvent(roomID id.RoomID, timestamp time.Time, dir Direction) (resp *RespTimestampToEvent, err error) { +func (cli *Client) TimestampToEvent(ctx context.Context, roomID id.RoomID, timestamp time.Time, dir Direction) (resp *RespTimestampToEvent, err error) { query := map[string]string{ "ts": strconv.FormatInt(timestamp.UnixMilli(), 10), "dir": string(dir), } urlPath := cli.BuildURLWithQuery(ClientURLPath{"v1", "rooms", roomID, "timestamp_to_event"}, query) - _, err = cli.MakeRequest(http.MethodGet, urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp) return } @@ -1743,7 +1796,7 @@ func (cli *Client) TimestampToEvent(roomID id.RoomID, timestamp time.Time, dir D // specified event. It use pagination query parameters to paginate history in // the room. // See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidcontexteventid -func (cli *Client) Context(roomID id.RoomID, eventID id.EventID, filter *FilterPart, limit int) (resp *RespContext, err error) { +func (cli *Client) Context(ctx context.Context, roomID id.RoomID, eventID id.EventID, filter *FilterPart, limit int) (resp *RespContext, err error) { query := map[string]string{} if filter != nil { filterJSON, err := json.Marshal(filter) @@ -1757,173 +1810,173 @@ func (cli *Client) Context(roomID id.RoomID, eventID id.EventID, filter *FilterP } urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "context", eventID}, query) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } -func (cli *Client) GetEvent(roomID id.RoomID, eventID id.EventID) (resp *event.Event, err error) { +func (cli *Client) GetEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID) (resp *event.Event, err error) { urlPath := cli.BuildClientURL("v3", "rooms", roomID, "event", eventID) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } -func (cli *Client) MarkRead(roomID id.RoomID, eventID id.EventID) (err error) { - return cli.SendReceipt(roomID, eventID, event.ReceiptTypeRead, nil) +func (cli *Client) MarkRead(ctx context.Context, roomID id.RoomID, eventID id.EventID) (err error) { + return cli.SendReceipt(ctx, roomID, eventID, event.ReceiptTypeRead, nil) } // MarkReadWithContent sends a read receipt including custom data. // // Deprecated: Use SendReceipt instead. -func (cli *Client) MarkReadWithContent(roomID id.RoomID, eventID id.EventID, content interface{}) (err error) { - return cli.SendReceipt(roomID, eventID, event.ReceiptTypeRead, content) +func (cli *Client) MarkReadWithContent(ctx context.Context, roomID id.RoomID, eventID id.EventID, content interface{}) (err error) { + return cli.SendReceipt(ctx, roomID, eventID, event.ReceiptTypeRead, content) } // SendReceipt sends a receipt, usually specifically a read receipt. // // Passing nil as the content is safe, the library will automatically replace it with an empty JSON object. // To mark a message in a specific thread as read, use pass a ReqSendReceipt as the content. -func (cli *Client) SendReceipt(roomID id.RoomID, eventID id.EventID, receiptType event.ReceiptType, content interface{}) (err error) { +func (cli *Client) SendReceipt(ctx context.Context, roomID id.RoomID, eventID id.EventID, receiptType event.ReceiptType, content interface{}) (err error) { urlPath := cli.BuildClientURL("v3", "rooms", roomID, "receipt", receiptType, eventID) - _, err = cli.MakeRequest("POST", urlPath, content, nil) + _, err = cli.MakeRequest(ctx, "POST", urlPath, content, nil) return } -func (cli *Client) SetReadMarkers(roomID id.RoomID, content interface{}) (err error) { +func (cli *Client) SetReadMarkers(ctx context.Context, roomID id.RoomID, content interface{}) (err error) { urlPath := cli.BuildClientURL("v3", "rooms", roomID, "read_markers") - _, err = cli.MakeRequest("POST", urlPath, content, nil) + _, err = cli.MakeRequest(ctx, "POST", urlPath, content, nil) return } -func (cli *Client) AddTag(roomID id.RoomID, tag string, order float64) error { +func (cli *Client) AddTag(ctx context.Context, roomID id.RoomID, tag string, order float64) error { var tagData event.Tag if order == order { tagData.Order = json.Number(strconv.FormatFloat(order, 'e', -1, 64)) } - return cli.AddTagWithCustomData(roomID, tag, tagData) + return cli.AddTagWithCustomData(ctx, roomID, tag, tagData) } -func (cli *Client) AddTagWithCustomData(roomID id.RoomID, tag string, data interface{}) (err error) { +func (cli *Client) AddTagWithCustomData(ctx context.Context, roomID id.RoomID, tag string, data interface{}) (err error) { urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags", tag) - _, err = cli.MakeRequest("PUT", urlPath, data, nil) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, data, nil) return } -func (cli *Client) GetTags(roomID id.RoomID) (tags event.TagEventContent, err error) { - err = cli.GetTagsWithCustomData(roomID, &tags) +func (cli *Client) GetTags(ctx context.Context, roomID id.RoomID) (tags event.TagEventContent, err error) { + err = cli.GetTagsWithCustomData(ctx, roomID, &tags) return } -func (cli *Client) GetTagsWithCustomData(roomID id.RoomID, resp interface{}) (err error) { +func (cli *Client) GetTagsWithCustomData(ctx context.Context, roomID id.RoomID, resp interface{}) (err error) { urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } -func (cli *Client) RemoveTag(roomID id.RoomID, tag string) (err error) { +func (cli *Client) RemoveTag(ctx context.Context, roomID id.RoomID, tag string) (err error) { urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags", tag) - _, err = cli.MakeRequest("DELETE", urlPath, nil, nil) + _, err = cli.MakeRequest(ctx, "DELETE", urlPath, nil, nil) return } // Deprecated: Synapse may not handle setting m.tag directly properly, so you should use the Add/RemoveTag methods instead. -func (cli *Client) SetTags(roomID id.RoomID, tags event.Tags) (err error) { - return cli.SetRoomAccountData(roomID, "m.tag", map[string]event.Tags{ +func (cli *Client) SetTags(ctx context.Context, roomID id.RoomID, tags event.Tags) (err error) { + return cli.SetRoomAccountData(ctx, roomID, "m.tag", map[string]event.Tags{ "tags": tags, }) } // TurnServer returns turn server details and credentials for the client to use when initiating calls. // See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3voipturnserver -func (cli *Client) TurnServer() (resp *RespTurnServer, err error) { +func (cli *Client) TurnServer(ctx context.Context) (resp *RespTurnServer, err error) { urlPath := cli.BuildClientURL("v3", "voip", "turnServer") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } -func (cli *Client) CreateAlias(alias id.RoomAlias, roomID id.RoomID) (resp *RespAliasCreate, err error) { +func (cli *Client) CreateAlias(ctx context.Context, alias id.RoomAlias, roomID id.RoomID) (resp *RespAliasCreate, err error) { urlPath := cli.BuildClientURL("v3", "directory", "room", alias) - _, err = cli.MakeRequest("PUT", urlPath, &ReqAliasCreate{RoomID: roomID}, &resp) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, &ReqAliasCreate{RoomID: roomID}, &resp) return } -func (cli *Client) ResolveAlias(alias id.RoomAlias) (resp *RespAliasResolve, err error) { +func (cli *Client) ResolveAlias(ctx context.Context, alias id.RoomAlias) (resp *RespAliasResolve, err error) { urlPath := cli.BuildClientURL("v3", "directory", "room", alias) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } -func (cli *Client) DeleteAlias(alias id.RoomAlias) (resp *RespAliasDelete, err error) { +func (cli *Client) DeleteAlias(ctx context.Context, alias id.RoomAlias) (resp *RespAliasDelete, err error) { urlPath := cli.BuildClientURL("v3", "directory", "room", alias) - _, err = cli.MakeRequest("DELETE", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "DELETE", urlPath, nil, &resp) return } -func (cli *Client) GetAliases(roomID id.RoomID) (resp *RespAliasList, err error) { +func (cli *Client) GetAliases(ctx context.Context, roomID id.RoomID) (resp *RespAliasList, err error) { urlPath := cli.BuildClientURL("v3", "rooms", roomID, "aliases") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } -func (cli *Client) UploadKeys(req *ReqUploadKeys) (resp *RespUploadKeys, err error) { +func (cli *Client) UploadKeys(ctx context.Context, req *ReqUploadKeys) (resp *RespUploadKeys, err error) { urlPath := cli.BuildClientURL("v3", "keys", "upload") - _, err = cli.MakeRequest("POST", urlPath, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp) return } -func (cli *Client) QueryKeys(req *ReqQueryKeys) (resp *RespQueryKeys, err error) { +func (cli *Client) QueryKeys(ctx context.Context, req *ReqQueryKeys) (resp *RespQueryKeys, err error) { urlPath := cli.BuildClientURL("v3", "keys", "query") - _, err = cli.MakeRequest("POST", urlPath, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp) return } -func (cli *Client) ClaimKeys(req *ReqClaimKeys) (resp *RespClaimKeys, err error) { +func (cli *Client) ClaimKeys(ctx context.Context, req *ReqClaimKeys) (resp *RespClaimKeys, err error) { urlPath := cli.BuildClientURL("v3", "keys", "claim") - _, err = cli.MakeRequest("POST", urlPath, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp) return } -func (cli *Client) GetKeyChanges(from, to string) (resp *RespKeyChanges, err error) { +func (cli *Client) GetKeyChanges(ctx context.Context, from, to string) (resp *RespKeyChanges, err error) { urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "keys", "changes"}, map[string]string{ "from": from, "to": to, }) - _, err = cli.MakeRequest("POST", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, nil, &resp) return } -func (cli *Client) SendToDevice(eventType event.Type, req *ReqSendToDevice) (resp *RespSendToDevice, err error) { +func (cli *Client) SendToDevice(ctx context.Context, eventType event.Type, req *ReqSendToDevice) (resp *RespSendToDevice, err error) { urlPath := cli.BuildClientURL("v3", "sendToDevice", eventType.String(), cli.TxnID()) - _, err = cli.MakeRequest("PUT", urlPath, req, &resp) + _, err = cli.MakeRequest(ctx, "PUT", urlPath, req, &resp) return } -func (cli *Client) GetDevicesInfo() (resp *RespDevicesInfo, err error) { +func (cli *Client) GetDevicesInfo(ctx context.Context) (resp *RespDevicesInfo, err error) { urlPath := cli.BuildClientURL("v3", "devices") - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } -func (cli *Client) GetDeviceInfo(deviceID id.DeviceID) (resp *RespDeviceInfo, err error) { +func (cli *Client) GetDeviceInfo(ctx context.Context, deviceID id.DeviceID) (resp *RespDeviceInfo, err error) { urlPath := cli.BuildClientURL("v3", "devices", deviceID) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) return } -func (cli *Client) SetDeviceInfo(deviceID id.DeviceID, req *ReqDeviceInfo) error { +func (cli *Client) SetDeviceInfo(ctx context.Context, deviceID id.DeviceID, req *ReqDeviceInfo) error { urlPath := cli.BuildClientURL("v3", "devices", deviceID) - _, err := cli.MakeRequest("PUT", urlPath, req, nil) + _, err := cli.MakeRequest(ctx, "PUT", urlPath, req, nil) return err } -func (cli *Client) DeleteDevice(deviceID id.DeviceID, req *ReqDeleteDevice) error { +func (cli *Client) DeleteDevice(ctx context.Context, deviceID id.DeviceID, req *ReqDeleteDevice) error { urlPath := cli.BuildClientURL("v3", "devices", deviceID) - _, err := cli.MakeRequest("DELETE", urlPath, req, nil) + _, err := cli.MakeRequest(ctx, "DELETE", urlPath, req, nil) return err } -func (cli *Client) DeleteDevices(req *ReqDeleteDevices) error { +func (cli *Client) DeleteDevices(ctx context.Context, req *ReqDeleteDevices) error { urlPath := cli.BuildClientURL("v3", "delete_devices") - _, err := cli.MakeRequest("DELETE", urlPath, req, nil) + _, err := cli.MakeRequest(ctx, "DELETE", urlPath, req, nil) return err } @@ -1932,8 +1985,8 @@ type UIACallback = func(*RespUserInteractive) interface{} // UploadCrossSigningKeys uploads the given cross-signing keys to the server. // Because the endpoint requires user-interactive authentication a callback must be provided that, // given the UI auth parameters, produces the required result (or nil to end the flow). -func (cli *Client) UploadCrossSigningKeys(keys *UploadCrossSigningKeysReq, uiaCallback UIACallback) error { - content, err := cli.MakeFullRequest(FullRequest{ +func (cli *Client) UploadCrossSigningKeys(ctx context.Context, keys *UploadCrossSigningKeysReq, uiaCallback UIACallback) error { + content, err := cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodPost, URL: cli.BuildClientURL("v3", "keys", "device_signing", "upload"), RequestJSON: keys, @@ -1948,48 +2001,48 @@ func (cli *Client) UploadCrossSigningKeys(keys *UploadCrossSigningKeysReq, uiaCa auth := uiaCallback(&uiAuthResp) if auth != nil { keys.Auth = auth - return cli.UploadCrossSigningKeys(keys, uiaCallback) + return cli.UploadCrossSigningKeys(ctx, keys, uiaCallback) } } return err } -func (cli *Client) UploadSignatures(req *ReqUploadSignatures) (resp *RespUploadSignatures, err error) { +func (cli *Client) UploadSignatures(ctx context.Context, req *ReqUploadSignatures) (resp *RespUploadSignatures, err error) { urlPath := cli.BuildClientURL("v3", "keys", "signatures", "upload") - _, err = cli.MakeRequest("POST", urlPath, req, &resp) + _, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp) return } // GetPushRules returns the push notification rules for the global scope. -func (cli *Client) GetPushRules() (*pushrules.PushRuleset, error) { - return cli.GetScopedPushRules("global") +func (cli *Client) GetPushRules(ctx context.Context) (*pushrules.PushRuleset, error) { + return cli.GetScopedPushRules(ctx, "global") } // GetScopedPushRules returns the push notification rules for the given scope. -func (cli *Client) GetScopedPushRules(scope string) (resp *pushrules.PushRuleset, err error) { +func (cli *Client) GetScopedPushRules(ctx context.Context, scope string) (resp *pushrules.PushRuleset, err error) { u, _ := url.Parse(cli.BuildClientURL("v3", "pushrules", scope)) // client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash. u.Path += "/" - _, err = cli.MakeRequest("GET", u.String(), nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", u.String(), nil, &resp) return } -func (cli *Client) GetPushRule(scope string, kind pushrules.PushRuleType, ruleID string) (resp *pushrules.PushRule, err error) { +func (cli *Client) GetPushRule(ctx context.Context, scope string, kind pushrules.PushRuleType, ruleID string) (resp *pushrules.PushRule, err error) { urlPath := cli.BuildClientURL("v3", "pushrules", scope, kind, ruleID) - _, err = cli.MakeRequest("GET", urlPath, nil, &resp) + _, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp) if resp != nil { resp.Type = kind } return } -func (cli *Client) DeletePushRule(scope string, kind pushrules.PushRuleType, ruleID string) error { +func (cli *Client) DeletePushRule(ctx context.Context, scope string, kind pushrules.PushRuleType, ruleID string) error { urlPath := cli.BuildClientURL("v3", "pushrules", scope, kind, ruleID) - _, err := cli.MakeRequest("DELETE", urlPath, nil, nil) + _, err := cli.MakeRequest(ctx, "DELETE", urlPath, nil, nil) return err } -func (cli *Client) PutPushRule(scope string, kind pushrules.PushRuleType, ruleID string, req *ReqPutPushRule) error { +func (cli *Client) PutPushRule(ctx context.Context, scope string, kind pushrules.PushRuleType, ruleID string, req *ReqPutPushRule) error { query := make(map[string]string) if len(req.After) > 0 { query["after"] = req.After @@ -1998,14 +2051,14 @@ func (cli *Client) PutPushRule(scope string, kind pushrules.PushRuleType, ruleID query["before"] = req.Before } urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "pushrules", scope, kind, ruleID}, query) - _, err := cli.MakeRequest("PUT", urlPath, req, nil) + _, err := cli.MakeRequest(ctx, "PUT", urlPath, req, nil) return err } // BatchSend sends a batch of historical events into a room. This is only available for appservices. // // Deprecated: MSC2716 has been abandoned, so this is now Beeper-specific. BeeperBatchSend should be used instead. -func (cli *Client) BatchSend(roomID id.RoomID, req *ReqBatchSend) (resp *RespBatchSend, err error) { +func (cli *Client) BatchSend(ctx context.Context, roomID id.RoomID, req *ReqBatchSend) (resp *RespBatchSend, err error) { path := ClientURLPath{"unstable", "org.matrix.msc2716", "rooms", roomID, "batch_send"} query := map[string]string{ "prev_event_id": req.PrevEventID.String(), @@ -2019,12 +2072,12 @@ func (cli *Client) BatchSend(roomID id.RoomID, req *ReqBatchSend) (resp *RespBat if len(req.BatchID) > 0 { query["batch_id"] = req.BatchID.String() } - _, err = cli.MakeRequest("POST", cli.BuildURLWithQuery(path, query), req, &resp) + _, err = cli.MakeRequest(ctx, "POST", cli.BuildURLWithQuery(path, query), req, &resp) return } -func (cli *Client) AppservicePing(id, txnID string) (resp *RespAppservicePing, err error) { - _, err = cli.MakeFullRequest(FullRequest{ +func (cli *Client) AppservicePing(ctx context.Context, id, txnID string) (resp *RespAppservicePing, err error) { + _, err = cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodPost, URL: cli.BuildClientURL("v1", "appservice", id, "ping"), RequestJSON: &ReqAppservicePing{TxnID: txnID}, @@ -2035,27 +2088,26 @@ func (cli *Client) AppservicePing(id, txnID string) (resp *RespAppservicePing, e return } -func (cli *Client) BeeperBatchSend(roomID id.RoomID, req *ReqBeeperBatchSend) (resp *RespBeeperBatchSend, err error) { +func (cli *Client) BeeperBatchSend(ctx context.Context, roomID id.RoomID, req *ReqBeeperBatchSend) (resp *RespBeeperBatchSend, err error) { u := cli.BuildClientURL("unstable", "com.beeper.backfill", "rooms", roomID, "batch_send") - _, err = cli.MakeRequest(http.MethodPost, u, req, &resp) + _, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp) return } -func (cli *Client) BeeperMergeRooms(req *ReqBeeperMergeRoom) (resp *RespBeeperMergeRoom, err error) { +func (cli *Client) BeeperMergeRooms(ctx context.Context, req *ReqBeeperMergeRoom) (resp *RespBeeperMergeRoom, err error) { urlPath := cli.BuildClientURL("unstable", "com.beeper.chatmerging", "merge") - _, err = cli.MakeRequest(http.MethodPost, urlPath, req, &resp) + _, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp) return } -func (cli *Client) BeeperSplitRoom(req *ReqBeeperSplitRoom) (resp *RespBeeperSplitRoom, err error) { +func (cli *Client) BeeperSplitRoom(ctx context.Context, req *ReqBeeperSplitRoom) (resp *RespBeeperSplitRoom, err error) { urlPath := cli.BuildClientURL("unstable", "com.beeper.chatmerging", "rooms", req.RoomID, "split") - _, err = cli.MakeRequest(http.MethodPost, urlPath, req, &resp) + _, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp) return } - -func (cli *Client) BeeperDeleteRoom(roomID id.RoomID) (err error) { +func (cli *Client) BeeperDeleteRoom(ctx context.Context, roomID id.RoomID) (err error) { urlPath := cli.BuildClientURL("unstable", "com.beeper.yeet", "rooms", roomID, "delete") - _, err = cli.MakeRequest(http.MethodPost, urlPath, nil, nil) + _, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, nil, nil) return } diff --git a/vendor/maunium.net/go/mautrix/crypto/account.go b/vendor/maunium.net/go/mautrix/crypto/account.go index a667825..0eb18a2 100644 --- a/vendor/maunium.net/go/mautrix/crypto/account.go +++ b/vendor/maunium.net/go/mautrix/crypto/account.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/vendor/maunium.net/go/mautrix/crypto/canonicaljson/LICENSE b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/LICENSE deleted file mode 100644 index f433b1a..0000000 --- a/vendor/maunium.net/go/mautrix/crypto/canonicaljson/LICENSE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md index 7f5d3da..da9d71f 100644 --- a/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md +++ b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md @@ -2,3 +2,5 @@ This is a Go package to produce Matrix [Canonical JSON](https://matrix.org/docs/spec/appendices#canonical-json). It is essentially just [json.go](https://github.com/matrix-org/gomatrixserverlib/blob/master/json.go) from gomatrixserverlib without all the other files that are completely useless for non-server use cases. + +The original project is licensed under the Apache 2.0 license. diff --git a/vendor/maunium.net/go/mautrix/crypto/cross_sign_key.go b/vendor/maunium.net/go/mautrix/crypto/cross_sign_key.go index d38df8f..4528ae0 100644 --- a/vendor/maunium.net/go/mautrix/crypto/cross_sign_key.go +++ b/vendor/maunium.net/go/mautrix/crypto/cross_sign_key.go @@ -8,6 +8,7 @@ package crypto import ( + "context" "fmt" "maunium.net/go/mautrix" @@ -89,7 +90,7 @@ func (mach *OlmMachine) GenerateCrossSigningKeys() (*CrossSigningKeysCache, erro } // PublishCrossSigningKeys signs and uploads the public keys of the given cross-signing keys to the server. -func (mach *OlmMachine) PublishCrossSigningKeys(keys *CrossSigningKeysCache, uiaCallback mautrix.UIACallback) error { +func (mach *OlmMachine) PublishCrossSigningKeys(ctx context.Context, keys *CrossSigningKeysCache, uiaCallback mautrix.UIACallback) error { userID := mach.Client.UserID masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey.String()) masterKey := mautrix.CrossSigningKeys{ @@ -134,7 +135,7 @@ func (mach *OlmMachine) PublishCrossSigningKeys(keys *CrossSigningKeysCache, uia }, } - err = mach.Client.UploadCrossSigningKeys(&mautrix.UploadCrossSigningKeysReq{ + err = mach.Client.UploadCrossSigningKeys(ctx, &mautrix.UploadCrossSigningKeysReq{ Master: masterKey, SelfSigning: selfKey, UserSigning: userKey, diff --git a/vendor/maunium.net/go/mautrix/crypto/cross_sign_pubkey.go b/vendor/maunium.net/go/mautrix/crypto/cross_sign_pubkey.go index 3067753..77efab5 100644 --- a/vendor/maunium.net/go/mautrix/crypto/cross_sign_pubkey.go +++ b/vendor/maunium.net/go/mautrix/crypto/cross_sign_pubkey.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,6 +7,7 @@ package crypto import ( + "context" "fmt" "maunium.net/go/mautrix" @@ -19,7 +20,7 @@ type CrossSigningPublicKeysCache struct { UserSigningKey id.Ed25519 } -func (mach *OlmMachine) GetOwnCrossSigningPublicKeys() *CrossSigningPublicKeysCache { +func (mach *OlmMachine) GetOwnCrossSigningPublicKeys(ctx context.Context) *CrossSigningPublicKeysCache { if mach.crossSigningPubkeys != nil { return mach.crossSigningPubkeys } @@ -30,7 +31,7 @@ func (mach *OlmMachine) GetOwnCrossSigningPublicKeys() *CrossSigningPublicKeysCa if mach.crossSigningPubkeysFetched { return nil } - cspk, err := mach.GetCrossSigningPublicKeys(mach.Client.UserID) + cspk, err := mach.GetCrossSigningPublicKeys(ctx, mach.Client.UserID) if err != nil { mach.Log.Error().Err(err).Msg("Failed to get own cross-signing public keys") return nil @@ -40,8 +41,8 @@ func (mach *OlmMachine) GetOwnCrossSigningPublicKeys() *CrossSigningPublicKeysCa return mach.crossSigningPubkeys } -func (mach *OlmMachine) GetCrossSigningPublicKeys(userID id.UserID) (*CrossSigningPublicKeysCache, error) { - dbKeys, err := mach.CryptoStore.GetCrossSigningKeys(userID) +func (mach *OlmMachine) GetCrossSigningPublicKeys(ctx context.Context, userID id.UserID) (*CrossSigningPublicKeysCache, error) { + dbKeys, err := mach.CryptoStore.GetCrossSigningKeys(ctx, userID) if err != nil { return nil, fmt.Errorf("failed to get keys from database: %w", err) } @@ -58,7 +59,7 @@ func (mach *OlmMachine) GetCrossSigningPublicKeys(userID id.UserID) (*CrossSigni } } - keys, err := mach.Client.QueryKeys(&mautrix.ReqQueryKeys{ + keys, err := mach.Client.QueryKeys(ctx, &mautrix.ReqQueryKeys{ DeviceKeys: mautrix.DeviceKeysRequest{ userID: mautrix.DeviceIDList{}, }, diff --git a/vendor/maunium.net/go/mautrix/crypto/cross_sign_signing.go b/vendor/maunium.net/go/mautrix/crypto/cross_sign_signing.go index 62c41b3..f6c37a9 100644 --- a/vendor/maunium.net/go/mautrix/crypto/cross_sign_signing.go +++ b/vendor/maunium.net/go/mautrix/crypto/cross_sign_signing.go @@ -1,5 +1,5 @@ // Copyright (c) 2020 Nikos Filippakis -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -8,6 +8,7 @@ package crypto import ( + "context" "errors" "fmt" @@ -33,8 +34,8 @@ var ( ErrMismatchingMasterKeyMAC = errors.New("mismatching cross-signing master key MAC") ) -func (mach *OlmMachine) fetchMasterKey(device *id.Device, content *event.VerificationMacEventContent, verState *verificationState, transactionID string) (id.Ed25519, error) { - crossSignKeys, err := mach.CryptoStore.GetCrossSigningKeys(device.UserID) +func (mach *OlmMachine) fetchMasterKey(ctx context.Context, device *id.Device, content *event.VerificationMacEventContent, verState *verificationState, transactionID string) (id.Ed25519, error) { + crossSignKeys, err := mach.CryptoStore.GetCrossSigningKeys(ctx, device.UserID) if err != nil { return "", fmt.Errorf("failed to fetch cross-signing keys: %w", err) } @@ -59,7 +60,7 @@ func (mach *OlmMachine) fetchMasterKey(device *id.Device, content *event.Verific } // SignUser creates a cross-signing signature for a user, stores it and uploads it to the server. -func (mach *OlmMachine) SignUser(userID id.UserID, masterKey id.Ed25519) error { +func (mach *OlmMachine) SignUser(ctx context.Context, userID id.UserID, masterKey id.Ed25519) error { if userID == mach.Client.UserID { return ErrCantSignOwnMasterKey } else if mach.CrossSigningKeys == nil || mach.CrossSigningKeys.UserSigningKey == nil { @@ -74,7 +75,7 @@ func (mach *OlmMachine) SignUser(userID id.UserID, masterKey id.Ed25519) error { }, } - signature, err := mach.signAndUpload(masterKeyObj, userID, masterKey.String(), mach.CrossSigningKeys.UserSigningKey) + signature, err := mach.signAndUpload(ctx, masterKeyObj, userID, masterKey.String(), mach.CrossSigningKeys.UserSigningKey) if err != nil { return err } @@ -84,7 +85,7 @@ func (mach *OlmMachine) SignUser(userID id.UserID, masterKey id.Ed25519) error { Str("signature", signature). Msg("Signed master key of user with our user-signing key") - if err := mach.CryptoStore.PutSignature(userID, masterKey, mach.Client.UserID, mach.CrossSigningKeys.UserSigningKey.PublicKey, signature); err != nil { + if err := mach.CryptoStore.PutSignature(ctx, userID, masterKey, mach.Client.UserID, mach.CrossSigningKeys.UserSigningKey.PublicKey, signature); err != nil { return fmt.Errorf("error storing signature in crypto store: %w", err) } @@ -92,7 +93,7 @@ func (mach *OlmMachine) SignUser(userID id.UserID, masterKey id.Ed25519) error { } // SignOwnMasterKey uses the current account for signing the current user's master key and uploads the signature. -func (mach *OlmMachine) SignOwnMasterKey() error { +func (mach *OlmMachine) SignOwnMasterKey(ctx context.Context) error { if mach.CrossSigningKeys == nil { return ErrCrossSigningKeysNotCached } else if mach.account == nil { @@ -124,7 +125,7 @@ func (mach *OlmMachine) SignOwnMasterKey() error { Str("signature", signature). Msg("Signed own master key with own device key") - resp, err := mach.Client.UploadSignatures(&mautrix.ReqUploadSignatures{ + resp, err := mach.Client.UploadSignatures(ctx, &mautrix.ReqUploadSignatures{ userID: map[string]mautrix.ReqKeysSignatures{ masterKey.String(): masterKeyObj, }, @@ -136,7 +137,7 @@ func (mach *OlmMachine) SignOwnMasterKey() error { return fmt.Errorf("%w: %+v", ErrSignatureUploadFail, resp.Failures) } - if err := mach.CryptoStore.PutSignature(userID, masterKey, userID, mach.account.SigningKey(), signature); err != nil { + if err := mach.CryptoStore.PutSignature(ctx, userID, masterKey, userID, mach.account.SigningKey(), signature); err != nil { return fmt.Errorf("error storing signature in crypto store: %w", err) } @@ -144,14 +145,14 @@ func (mach *OlmMachine) SignOwnMasterKey() error { } // SignOwnDevice creates a cross-signing signature for a device belonging to the current user and uploads it to the server. -func (mach *OlmMachine) SignOwnDevice(device *id.Device) error { +func (mach *OlmMachine) SignOwnDevice(ctx context.Context, device *id.Device) error { if device.UserID != mach.Client.UserID { return ErrCantSignOtherDevice } else if mach.CrossSigningKeys == nil || mach.CrossSigningKeys.SelfSigningKey == nil { return ErrSelfSigningKeyNotCached } - deviceKeys, err := mach.getFullDeviceKeys(device) + deviceKeys, err := mach.getFullDeviceKeys(ctx, device) if err != nil { return err } @@ -166,7 +167,7 @@ func (mach *OlmMachine) SignOwnDevice(device *id.Device) error { deviceKeyObj.Keys[id.KeyID(keyID)] = key } - signature, err := mach.signAndUpload(deviceKeyObj, device.UserID, device.DeviceID.String(), mach.CrossSigningKeys.SelfSigningKey) + signature, err := mach.signAndUpload(ctx, deviceKeyObj, device.UserID, device.DeviceID.String(), mach.CrossSigningKeys.SelfSigningKey) if err != nil { return err } @@ -177,7 +178,7 @@ func (mach *OlmMachine) SignOwnDevice(device *id.Device) error { Str("signature", signature). Msg("Signed own device key with self-signing key") - if err := mach.CryptoStore.PutSignature(device.UserID, device.SigningKey, mach.Client.UserID, mach.CrossSigningKeys.SelfSigningKey.PublicKey, signature); err != nil { + if err := mach.CryptoStore.PutSignature(ctx, device.UserID, device.SigningKey, mach.Client.UserID, mach.CrossSigningKeys.SelfSigningKey.PublicKey, signature); err != nil { return fmt.Errorf("error storing signature in crypto store: %w", err) } @@ -186,8 +187,8 @@ func (mach *OlmMachine) SignOwnDevice(device *id.Device) error { // getFullDeviceKeys gets the full device keys object for the given device. // This is used because we don't cache some of the details like list of algorithms and unsupported key types. -func (mach *OlmMachine) getFullDeviceKeys(device *id.Device) (*mautrix.DeviceKeys, error) { - devicesKeys, err := mach.Client.QueryKeys(&mautrix.ReqQueryKeys{ +func (mach *OlmMachine) getFullDeviceKeys(ctx context.Context, device *id.Device) (*mautrix.DeviceKeys, error) { + devicesKeys, err := mach.Client.QueryKeys(ctx, &mautrix.ReqQueryKeys{ DeviceKeys: mautrix.DeviceKeysRequest{ device.UserID: mautrix.DeviceIDList{device.DeviceID}, }, @@ -208,7 +209,7 @@ func (mach *OlmMachine) getFullDeviceKeys(device *id.Device) (*mautrix.DeviceKey } // signAndUpload signs the given key signatures object and uploads it to the server. -func (mach *OlmMachine) signAndUpload(req mautrix.ReqKeysSignatures, userID id.UserID, signedThing string, key *olm.PkSigning) (string, error) { +func (mach *OlmMachine) signAndUpload(ctx context.Context, req mautrix.ReqKeysSignatures, userID id.UserID, signedThing string, key *olm.PkSigning) (string, error) { signature, err := key.SignJSON(req) if err != nil { return "", fmt.Errorf("failed to sign JSON: %w", err) @@ -219,7 +220,7 @@ func (mach *OlmMachine) signAndUpload(req mautrix.ReqKeysSignatures, userID id.U }, } - resp, err := mach.Client.UploadSignatures(&mautrix.ReqUploadSignatures{ + resp, err := mach.Client.UploadSignatures(ctx, &mautrix.ReqUploadSignatures{ userID: map[string]mautrix.ReqKeysSignatures{ signedThing: req, }, diff --git a/vendor/maunium.net/go/mautrix/crypto/cross_sign_ssss.go b/vendor/maunium.net/go/mautrix/crypto/cross_sign_ssss.go index b8ca71c..ef8a0ad 100644 --- a/vendor/maunium.net/go/mautrix/crypto/cross_sign_ssss.go +++ b/vendor/maunium.net/go/mautrix/crypto/cross_sign_ssss.go @@ -7,6 +7,7 @@ package crypto import ( + "context" "fmt" "maunium.net/go/mautrix" @@ -16,16 +17,16 @@ import ( ) // FetchCrossSigningKeysFromSSSS fetches all the cross-signing keys from SSSS, decrypts them using the given key and stores them in the olm machine. -func (mach *OlmMachine) FetchCrossSigningKeysFromSSSS(key *ssss.Key) error { - masterKey, err := mach.retrieveDecryptXSigningKey(event.AccountDataCrossSigningMaster, key) +func (mach *OlmMachine) FetchCrossSigningKeysFromSSSS(ctx context.Context, key *ssss.Key) error { + masterKey, err := mach.retrieveDecryptXSigningKey(ctx, event.AccountDataCrossSigningMaster, key) if err != nil { return err } - selfSignKey, err := mach.retrieveDecryptXSigningKey(event.AccountDataCrossSigningSelf, key) + selfSignKey, err := mach.retrieveDecryptXSigningKey(ctx, event.AccountDataCrossSigningSelf, key) if err != nil { return err } - userSignKey, err := mach.retrieveDecryptXSigningKey(event.AccountDataCrossSigningUser, key) + userSignKey, err := mach.retrieveDecryptXSigningKey(ctx, event.AccountDataCrossSigningUser, key) if err != nil { return err } @@ -38,12 +39,12 @@ func (mach *OlmMachine) FetchCrossSigningKeysFromSSSS(key *ssss.Key) error { } // retrieveDecryptXSigningKey retrieves the requested cross-signing key from SSSS and decrypts it using the given SSSS key. -func (mach *OlmMachine) retrieveDecryptXSigningKey(keyName event.Type, key *ssss.Key) ([utils.AESCTRKeyLength]byte, error) { +func (mach *OlmMachine) retrieveDecryptXSigningKey(ctx context.Context, keyName event.Type, key *ssss.Key) ([utils.AESCTRKeyLength]byte, error) { var decryptedKey [utils.AESCTRKeyLength]byte var encData ssss.EncryptedAccountDataEventContent // retrieve and parse the account data for this key type from SSSS - err := mach.Client.GetAccountData(keyName.Type, &encData) + err := mach.Client.GetAccountData(ctx, keyName.Type, &encData) if err != nil { return decryptedKey, err } @@ -62,8 +63,8 @@ func (mach *OlmMachine) retrieveDecryptXSigningKey(keyName event.Type, key *ssss // is used. The base58-formatted recovery key is the first return parameter. // // The account password of the user is required for uploading keys to the server. -func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(userPassword, passphrase string) (string, error) { - key, err := mach.SSSS.GenerateAndUploadKey(passphrase) +func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(ctx context.Context, userPassword, passphrase string) (string, error) { + key, err := mach.SSSS.GenerateAndUploadKey(ctx, passphrase) if err != nil { return "", fmt.Errorf("failed to generate and upload SSSS key: %w", err) } @@ -77,12 +78,12 @@ func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(userPassword, passphra recoveryKey := key.RecoveryKey() // Store the private keys in SSSS - if err := mach.UploadCrossSigningKeysToSSSS(key, keysCache); err != nil { + if err := mach.UploadCrossSigningKeysToSSSS(ctx, key, keysCache); err != nil { return recoveryKey, fmt.Errorf("failed to upload cross-signing keys to SSSS: %w", err) } // Publish cross-signing keys - err = mach.PublishCrossSigningKeys(keysCache, func(uiResp *mautrix.RespUserInteractive) interface{} { + err = mach.PublishCrossSigningKeys(ctx, keysCache, func(uiResp *mautrix.RespUserInteractive) interface{} { return &mautrix.ReqUIAuthLogin{ BaseAuthData: mautrix.BaseAuthData{ Type: mautrix.AuthTypePassword, @@ -96,7 +97,7 @@ func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(userPassword, passphra return recoveryKey, fmt.Errorf("failed to publish cross-signing keys: %w", err) } - err = mach.SSSS.SetDefaultKeyID(key.ID) + err = mach.SSSS.SetDefaultKeyID(ctx, key.ID) if err != nil { return recoveryKey, fmt.Errorf("failed to mark %s as the default key: %w", key.ID, err) } @@ -105,14 +106,14 @@ func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(userPassword, passphra } // UploadCrossSigningKeysToSSSS stores the given cross-signing keys on the server encrypted with the given key. -func (mach *OlmMachine) UploadCrossSigningKeysToSSSS(key *ssss.Key, keys *CrossSigningKeysCache) error { - if err := mach.SSSS.SetEncryptedAccountData(event.AccountDataCrossSigningMaster, keys.MasterKey.Seed, key); err != nil { +func (mach *OlmMachine) UploadCrossSigningKeysToSSSS(ctx context.Context, key *ssss.Key, keys *CrossSigningKeysCache) error { + if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningMaster, keys.MasterKey.Seed, key); err != nil { return err } - if err := mach.SSSS.SetEncryptedAccountData(event.AccountDataCrossSigningSelf, keys.SelfSigningKey.Seed, key); err != nil { + if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningSelf, keys.SelfSigningKey.Seed, key); err != nil { return err } - if err := mach.SSSS.SetEncryptedAccountData(event.AccountDataCrossSigningUser, keys.UserSigningKey.Seed, key); err != nil { + if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningUser, keys.UserSigningKey.Seed, key); err != nil { return err } return nil diff --git a/vendor/maunium.net/go/mautrix/crypto/cross_sign_store.go b/vendor/maunium.net/go/mautrix/crypto/cross_sign_store.go index f1008eb..88fcd0e 100644 --- a/vendor/maunium.net/go/mautrix/crypto/cross_sign_store.go +++ b/vendor/maunium.net/go/mautrix/crypto/cross_sign_store.go @@ -1,5 +1,5 @@ // Copyright (c) 2020 Nikos Filippakis -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -19,7 +19,7 @@ func (mach *OlmMachine) storeCrossSigningKeys(ctx context.Context, crossSigningK log := mach.machOrContextLog(ctx) for userID, userKeys := range crossSigningKeys { log := log.With().Str("user_id", userID.String()).Logger() - currentKeys, err := mach.CryptoStore.GetCrossSigningKeys(userID) + currentKeys, err := mach.CryptoStore.GetCrossSigningKeys(ctx, userID) if err != nil { log.Error().Err(err). Msg("Error fetching current cross-signing keys of user") @@ -32,7 +32,7 @@ func (mach *OlmMachine) storeCrossSigningKeys(ctx context.Context, crossSigningK if newKeyUsage == curKeyUsage { if _, ok := userKeys.Keys[id.NewKeyID(id.KeyAlgorithmEd25519, curKey.Key.String())]; !ok { // old key is not in the new key map, so we drop signatures made by it - if count, err := mach.CryptoStore.DropSignaturesByKey(userID, curKey.Key); err != nil { + if count, err := mach.CryptoStore.DropSignaturesByKey(ctx, userID, curKey.Key); err != nil { log.Error().Err(err).Msg("Error deleting old signatures made by user") } else { log.Debug(). @@ -50,7 +50,7 @@ func (mach *OlmMachine) storeCrossSigningKeys(ctx context.Context, crossSigningK log := log.With().Str("key", key.String()).Strs("usages", strishArray(userKeys.Usage)).Logger() for _, usage := range userKeys.Usage { log.Debug().Str("usage", string(usage)).Msg("Storing cross-signing key") - if err = mach.CryptoStore.PutCrossSigningKey(userID, usage, key); err != nil { + if err = mach.CryptoStore.PutCrossSigningKey(ctx, userID, usage, key); err != nil { log.Error().Err(err).Msg("Error storing cross-signing key") } } @@ -85,7 +85,7 @@ func (mach *OlmMachine) storeCrossSigningKeys(ctx context.Context, crossSigningK } else { if verified { log.Debug().Err(err).Msg("Cross-signing key signature verified") - err = mach.CryptoStore.PutSignature(userID, key, signUserID, signingKey, signature) + err = mach.CryptoStore.PutSignature(ctx, userID, key, signUserID, signingKey, signature) if err != nil { log.Error().Err(err).Msg("Error storing cross-signing key signature") } diff --git a/vendor/maunium.net/go/mautrix/crypto/cross_sign_validation.go b/vendor/maunium.net/go/mautrix/crypto/cross_sign_validation.go index e8a7d79..ff2452e 100644 --- a/vendor/maunium.net/go/mautrix/crypto/cross_sign_validation.go +++ b/vendor/maunium.net/go/mautrix/crypto/cross_sign_validation.go @@ -1,5 +1,5 @@ // Copyright (c) 2020 Nikos Filippakis -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -23,7 +23,7 @@ func (mach *OlmMachine) ResolveTrustContext(ctx context.Context, device *id.Devi if device.Trust == id.TrustStateVerified || device.Trust == id.TrustStateBlacklisted { return device.Trust, nil } - theirKeys, err := mach.CryptoStore.GetCrossSigningKeys(device.UserID) + theirKeys, err := mach.CryptoStore.GetCrossSigningKeys(ctx, device.UserID) if err != nil { mach.machOrContextLog(ctx).Error().Err(err). Str("user_id", device.UserID.String()). @@ -44,7 +44,7 @@ func (mach *OlmMachine) ResolveTrustContext(ctx context.Context, device *id.Devi Msg("Self-signing key of user not found") return id.TrustStateUnset, nil } - sskSigExists, err := mach.CryptoStore.IsKeySignedBy(device.UserID, theirSSK.Key, device.UserID, theirMSK.Key) + sskSigExists, err := mach.CryptoStore.IsKeySignedBy(ctx, device.UserID, theirSSK.Key, device.UserID, theirMSK.Key) if err != nil { mach.machOrContextLog(ctx).Error().Err(err). Str("user_id", device.UserID.String()). @@ -57,7 +57,7 @@ func (mach *OlmMachine) ResolveTrustContext(ctx context.Context, device *id.Devi Msg("Self-signing key of user is not signed by their master key") return id.TrustStateUnset, nil } - deviceSigExists, err := mach.CryptoStore.IsKeySignedBy(device.UserID, device.SigningKey, device.UserID, theirSSK.Key) + deviceSigExists, err := mach.CryptoStore.IsKeySignedBy(ctx, device.UserID, device.SigningKey, device.UserID, theirSSK.Key) if err != nil { mach.machOrContextLog(ctx).Error().Err(err). Str("user_id", device.UserID.String()). @@ -89,7 +89,7 @@ func (mach *OlmMachine) IsDeviceTrusted(device *id.Device) bool { // IsUserTrusted returns whether a user has been determined to be trusted by our user-signing key having signed their master key. // In the case the user ID is our own and we have successfully retrieved our cross-signing keys, we trust our own user. func (mach *OlmMachine) IsUserTrusted(ctx context.Context, userID id.UserID) (bool, error) { - csPubkeys := mach.GetOwnCrossSigningPublicKeys() + csPubkeys := mach.GetOwnCrossSigningPublicKeys(ctx) if csPubkeys == nil { return false, nil } @@ -97,14 +97,14 @@ func (mach *OlmMachine) IsUserTrusted(ctx context.Context, userID id.UserID) (bo return true, nil } // first we verify our user-signing key - ourUserSigningKeyTrusted, err := mach.CryptoStore.IsKeySignedBy(mach.Client.UserID, csPubkeys.UserSigningKey, mach.Client.UserID, csPubkeys.MasterKey) + ourUserSigningKeyTrusted, err := mach.CryptoStore.IsKeySignedBy(ctx, mach.Client.UserID, csPubkeys.UserSigningKey, mach.Client.UserID, csPubkeys.MasterKey) if err != nil { mach.machOrContextLog(ctx).Error().Err(err).Msg("Error retrieving our self-signing key signatures from database") return false, err } else if !ourUserSigningKeyTrusted { return false, nil } - theirKeys, err := mach.CryptoStore.GetCrossSigningKeys(userID) + theirKeys, err := mach.CryptoStore.GetCrossSigningKeys(ctx, userID) if err != nil { mach.machOrContextLog(ctx).Error().Err(err). Str("user_id", userID.String()). @@ -118,7 +118,7 @@ func (mach *OlmMachine) IsUserTrusted(ctx context.Context, userID id.UserID) (bo Msg("Master key of user not found") return false, nil } - sigExists, err := mach.CryptoStore.IsKeySignedBy(userID, theirMskKey.Key, mach.Client.UserID, csPubkeys.UserSigningKey) + sigExists, err := mach.CryptoStore.IsKeySignedBy(ctx, userID, theirMskKey.Key, mach.Client.UserID, csPubkeys.UserSigningKey) if err != nil { mach.machOrContextLog(ctx).Error().Err(err). Str("user_id", userID.String()). diff --git a/vendor/maunium.net/go/mautrix/crypto/cryptohelper/cryptohelper.go b/vendor/maunium.net/go/mautrix/crypto/cryptohelper/cryptohelper.go index 35dadac..a006501 100644 --- a/vendor/maunium.net/go/mautrix/crypto/cryptohelper/cryptohelper.go +++ b/vendor/maunium.net/go/mautrix/crypto/cryptohelper/cryptohelper.go @@ -105,7 +105,7 @@ func NewCryptoHelper(cli *mautrix.Client, pickleKey []byte, store any) (*CryptoH }, nil } -func (helper *CryptoHelper) Init() error { +func (helper *CryptoHelper) Init(ctx context.Context) error { if helper == nil { return fmt.Errorf("crypto helper is nil") } @@ -116,7 +116,7 @@ func (helper *CryptoHelper) Init() error { var stateStore crypto.StateStore if helper.managedStateStore != nil { - err := helper.managedStateStore.Upgrade() + err := helper.managedStateStore.Upgrade(ctx) if err != nil { return fmt.Errorf("failed to upgrade client state store: %w", err) } @@ -132,11 +132,14 @@ func (helper *CryptoHelper) Init() error { } else if _, isMemory := helper.client.Store.(*mautrix.MemorySyncStore); isMemory { helper.client.Store = managedCryptoStore } - err := managedCryptoStore.DB.Upgrade() + err := managedCryptoStore.DB.Upgrade(ctx) if err != nil { return fmt.Errorf("failed to upgrade crypto state store: %w", err) } - storedDeviceID := managedCryptoStore.FindDeviceID() + storedDeviceID, err := managedCryptoStore.FindDeviceID(ctx) + if err != nil { + return fmt.Errorf("failed to find existing device ID: %w", err) + } if helper.LoginAs != nil { if storedDeviceID != "" { helper.LoginAs.DeviceID = storedDeviceID @@ -146,7 +149,7 @@ func (helper *CryptoHelper) Init() error { Str("username", helper.LoginAs.Identifier.User). Str("device_id", helper.LoginAs.DeviceID.String()). Msg("Logging in") - _, err = helper.client.Login(helper.LoginAs) + _, err = helper.client.Login(ctx, helper.LoginAs) if err != nil { return err } @@ -167,10 +170,10 @@ func (helper *CryptoHelper) Init() error { return fmt.Errorf("the client must be logged in") } helper.mach = crypto.NewOlmMachine(helper.client, &helper.log, cryptoStore, stateStore) - err := helper.mach.Load() + err := helper.mach.Load(ctx) if err != nil { return fmt.Errorf("failed to load olm account: %w", err) - } else if err = helper.verifyDeviceKeysOnServer(); err != nil { + } else if err = helper.verifyDeviceKeysOnServer(ctx); err != nil { return err } @@ -204,9 +207,9 @@ func (helper *CryptoHelper) Machine() *crypto.OlmMachine { return helper.mach } -func (helper *CryptoHelper) verifyDeviceKeysOnServer() error { +func (helper *CryptoHelper) verifyDeviceKeysOnServer(ctx context.Context) error { helper.log.Debug().Msg("Making sure our device has the expected keys on the server") - resp, err := helper.client.QueryKeys(&mautrix.ReqQueryKeys{ + resp, err := helper.client.QueryKeys(ctx, &mautrix.ReqQueryKeys{ DeviceKeys: map[id.UserID]mautrix.DeviceIDList{ helper.client.UserID: {helper.client.DeviceID}, }, @@ -242,27 +245,29 @@ var NoSessionFound = crypto.NoSessionFound const initialSessionWaitTimeout = 3 * time.Second const extendedSessionWaitTimeout = 22 * time.Second -func (helper *CryptoHelper) HandleEncrypted(src mautrix.EventSource, evt *event.Event) { +func (helper *CryptoHelper) HandleEncrypted(ctx context.Context, evt *event.Event) { if helper == nil { return } content := evt.Content.AsEncrypted() + // TODO use context log instead of helper? log := helper.log.With(). Str("event_id", evt.ID.String()). Str("session_id", content.SessionID.String()). Logger() log.Debug().Msg("Decrypting received event") + ctx = log.WithContext(ctx) - decrypted, err := helper.Decrypt(evt) + decrypted, err := helper.Decrypt(ctx, evt) if errors.Is(err, NoSessionFound) { log.Debug(). Int("wait_seconds", int(initialSessionWaitTimeout.Seconds())). Msg("Couldn't find session, waiting for keys to arrive...") - if helper.mach.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, initialSessionWaitTimeout) { + if helper.mach.WaitForSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, initialSessionWaitTimeout) { log.Debug().Msg("Got keys after waiting, trying to decrypt event again") - decrypted, err = helper.Decrypt(evt) + decrypted, err = helper.Decrypt(ctx, evt) } else { - go helper.waitLongerForSession(log, src, evt) + go helper.waitLongerForSession(ctx, log, evt) return } } @@ -271,14 +276,15 @@ func (helper *CryptoHelper) HandleEncrypted(src mautrix.EventSource, evt *event. helper.DecryptErrorCallback(evt, err) return } - helper.postDecrypt(src, decrypted) + helper.postDecrypt(ctx, decrypted) } -func (helper *CryptoHelper) postDecrypt(src mautrix.EventSource, decrypted *event.Event) { - helper.client.Syncer.(mautrix.DispatchableSyncer).Dispatch(src|mautrix.EventSourceDecrypted, decrypted) +func (helper *CryptoHelper) postDecrypt(ctx context.Context, decrypted *event.Event) { + decrypted.Mautrix.EventSource |= event.SourceDecrypted + helper.client.Syncer.(mautrix.DispatchableSyncer).Dispatch(ctx, decrypted) } -func (helper *CryptoHelper) RequestSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, userID id.UserID, deviceID id.DeviceID) { +func (helper *CryptoHelper) RequestSession(ctx context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, userID id.UserID, deviceID id.DeviceID) { if helper == nil { return } @@ -294,7 +300,7 @@ func (helper *CryptoHelper) RequestSession(roomID id.RoomID, senderKey id.Sender Str("device_id", deviceID.String()). Str("room_id", roomID.String()). Logger() - err := helper.mach.SendRoomKeyRequest(roomID, senderKey, sessionID, "", map[id.UserID][]id.DeviceID{ + err := helper.mach.SendRoomKeyRequest(ctx, roomID, senderKey, sessionID, "", map[id.UserID][]id.DeviceID{ userID: {deviceID}, helper.client.UserID: {"*"}, }) @@ -305,55 +311,54 @@ func (helper *CryptoHelper) RequestSession(roomID id.RoomID, senderKey id.Sender } } -func (helper *CryptoHelper) waitLongerForSession(log zerolog.Logger, src mautrix.EventSource, evt *event.Event) { +func (helper *CryptoHelper) waitLongerForSession(ctx context.Context, log zerolog.Logger, evt *event.Event) { content := evt.Content.AsEncrypted() log.Debug().Int("wait_seconds", int(extendedSessionWaitTimeout.Seconds())).Msg("Couldn't find session, requesting keys and waiting longer...") - go helper.RequestSession(evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID) + go helper.RequestSession(context.TODO(), evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID) - if !helper.mach.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, extendedSessionWaitTimeout) { + if !helper.mach.WaitForSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, extendedSessionWaitTimeout) { log.Debug().Msg("Didn't get session, giving up") helper.DecryptErrorCallback(evt, NoSessionFound) return } log.Debug().Msg("Got keys after waiting longer, trying to decrypt event again") - decrypted, err := helper.Decrypt(evt) + decrypted, err := helper.Decrypt(ctx, evt) if err != nil { log.Error().Err(err).Msg("Failed to decrypt event") helper.DecryptErrorCallback(evt, err) return } - helper.postDecrypt(src, decrypted) + helper.postDecrypt(ctx, decrypted) } -func (helper *CryptoHelper) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool { +func (helper *CryptoHelper) WaitForSession(ctx context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool { if helper == nil { return false } helper.lock.RLock() defer helper.lock.RUnlock() - return helper.mach.WaitForSession(roomID, senderKey, sessionID, timeout) + return helper.mach.WaitForSession(ctx, roomID, senderKey, sessionID, timeout) } -func (helper *CryptoHelper) Decrypt(evt *event.Event) (*event.Event, error) { +func (helper *CryptoHelper) Decrypt(ctx context.Context, evt *event.Event) (*event.Event, error) { if helper == nil { return nil, fmt.Errorf("crypto helper is nil") } - return helper.mach.DecryptMegolmEvent(context.TODO(), evt) + return helper.mach.DecryptMegolmEvent(ctx, evt) } -func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, content any) (encrypted *event.EncryptedEventContent, err error) { +func (helper *CryptoHelper) Encrypt(ctx context.Context, roomID id.RoomID, evtType event.Type, content any) (encrypted *event.EncryptedEventContent, err error) { if helper == nil { return nil, fmt.Errorf("crypto helper is nil") } helper.lock.RLock() defer helper.lock.RUnlock() - ctx := context.TODO() encrypted, err = helper.mach.EncryptMegolmEvent(ctx, roomID, evtType, content) if err != nil { - if err != crypto.SessionExpired && err != crypto.SessionNotShared && err != crypto.NoGroupSession { + if !errors.Is(err, crypto.SessionExpired) && err != crypto.NoGroupSession && !errors.Is(err, crypto.SessionNotShared) { return } helper.log.Debug(). @@ -361,7 +366,7 @@ func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, conten Str("room_id", roomID.String()). Msg("Got session error while encrypting event, sharing group session and trying again") var users []id.UserID - users, err = helper.client.StateStore.GetRoomJoinedOrInvitedMembers(roomID) + users, err = helper.client.StateStore.GetRoomJoinedOrInvitedMembers(ctx, roomID) if err != nil { err = fmt.Errorf("failed to get room member list: %w", err) } else if err = helper.mach.ShareGroupSession(ctx, roomID, users); err != nil { diff --git a/vendor/maunium.net/go/mautrix/crypto/decryptmegolm.go b/vendor/maunium.net/go/mautrix/crypto/decryptmegolm.go index eaff136..540f99c 100644 --- a/vendor/maunium.net/go/mautrix/crypto/decryptmegolm.go +++ b/vendor/maunium.net/go/mautrix/crypto/decryptmegolm.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -91,7 +91,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(ctx context.Context, evt *event.Event } else { forwardedKeys = true lastChainItem := sess.ForwardingChains[len(sess.ForwardingChains)-1] - device, _ = mach.CryptoStore.FindDeviceByKey(evt.Sender, id.IdentityKey(lastChainItem)) + device, _ = mach.CryptoStore.FindDeviceByKey(ctx, evt.Sender, id.IdentityKey(lastChainItem)) if device != nil { trustLevel = mach.ResolveTrust(device) } else { @@ -188,7 +188,7 @@ func (mach *OlmMachine) actuallyDecryptMegolmEvent(ctx context.Context, evt *eve mach.megolmDecryptLock.Lock() defer mach.megolmDecryptLock.Unlock() - sess, err := mach.CryptoStore.GetGroupSession(encryptionRoomID, content.SenderKey, content.SessionID) + sess, err := mach.CryptoStore.GetGroupSession(ctx, encryptionRoomID, content.SenderKey, content.SessionID) if err != nil { return nil, nil, 0, fmt.Errorf("failed to get group session: %w", err) } else if sess == nil { @@ -250,7 +250,7 @@ func (mach *OlmMachine) actuallyDecryptMegolmEvent(ctx context.Context, evt *eve Int("max_messages", sess.MaxMessages). Logger() if sess.MaxMessages > 0 && int(ratchetTargetIndex) >= sess.MaxMessages && len(sess.RatchetSafety.MissedIndices) == 0 && mach.DeleteFullyUsedKeysOnDecrypt { - err = mach.CryptoStore.RedactGroupSession(sess.RoomID, sess.SenderKey, sess.ID(), "maximum messages reached") + err = mach.CryptoStore.RedactGroupSession(ctx, sess.RoomID, sess.SenderKey, sess.ID(), "maximum messages reached") if err != nil { log.Err(err).Msg("Failed to delete fully used session") return sess, plaintext, messageIndex, RatchetError @@ -261,14 +261,14 @@ func (mach *OlmMachine) actuallyDecryptMegolmEvent(ctx context.Context, evt *eve if err = sess.RatchetTo(ratchetTargetIndex); err != nil { log.Err(err).Msg("Failed to ratchet session") return sess, plaintext, messageIndex, RatchetError - } else if err = mach.CryptoStore.PutGroupSession(sess.RoomID, sess.SenderKey, sess.ID(), sess); err != nil { + } else if err = mach.CryptoStore.PutGroupSession(ctx, sess.RoomID, sess.SenderKey, sess.ID(), sess); err != nil { log.Err(err).Msg("Failed to store ratcheted session") return sess, plaintext, messageIndex, RatchetError } else { log.Info().Msg("Ratcheted session forward") } } else if didModify { - if err = mach.CryptoStore.PutGroupSession(sess.RoomID, sess.SenderKey, sess.ID(), sess); err != nil { + if err = mach.CryptoStore.PutGroupSession(ctx, sess.RoomID, sess.SenderKey, sess.ID(), sess); err != nil { log.Err(err).Msg("Failed to store updated ratchet safety data") return sess, plaintext, messageIndex, RatchetError } else { diff --git a/vendor/maunium.net/go/mautrix/crypto/decryptolm.go b/vendor/maunium.net/go/mautrix/crypto/decryptolm.go index e5394e5..68eaa87 100644 --- a/vendor/maunium.net/go/mautrix/crypto/decryptolm.go +++ b/vendor/maunium.net/go/mautrix/crypto/decryptolm.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -159,7 +159,7 @@ func (mach *OlmMachine) tryDecryptOlmCiphertext(ctx context.Context, sender id.U } endTimeTrace = mach.timeTrace(ctx, "updating new session in database", time.Second) - err = mach.CryptoStore.UpdateSession(senderKey, session) + err = mach.CryptoStore.UpdateSession(ctx, senderKey, session) endTimeTrace() if err != nil { log.Warn().Err(err).Msg("Failed to update new olm session in crypto store after decrypting") @@ -170,7 +170,7 @@ func (mach *OlmMachine) tryDecryptOlmCiphertext(ctx context.Context, sender id.U func (mach *OlmMachine) tryDecryptOlmCiphertextWithExistingSession(ctx context.Context, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) ([]byte, error) { log := *zerolog.Ctx(ctx) endTimeTrace := mach.timeTrace(ctx, "getting sessions with sender key", time.Second) - sessions, err := mach.CryptoStore.GetSessions(senderKey) + sessions, err := mach.CryptoStore.GetSessions(ctx, senderKey) endTimeTrace() if err != nil { return nil, fmt.Errorf("failed to get session for %s: %w", senderKey, err) @@ -199,7 +199,7 @@ func (mach *OlmMachine) tryDecryptOlmCiphertextWithExistingSession(ctx context.C } } else { endTimeTrace = mach.timeTrace(ctx, "updating session in database", time.Second) - err = mach.CryptoStore.UpdateSession(senderKey, session) + err = mach.CryptoStore.UpdateSession(ctx, senderKey, session) endTimeTrace() if err != nil { log.Warn().Err(err).Msg("Failed to update olm session in crypto store after decrypting") @@ -216,8 +216,8 @@ func (mach *OlmMachine) createInboundSession(ctx context.Context, senderKey id.S if err != nil { return nil, err } - mach.saveAccount() - err = mach.CryptoStore.AddSession(senderKey, session) + mach.saveAccount(ctx) + err = mach.CryptoStore.AddSession(ctx, senderKey, session) if err != nil { zerolog.Ctx(ctx).Error().Err(err).Msg("Failed to store created inbound session") } @@ -228,7 +228,7 @@ const MinUnwedgeInterval = 1 * time.Hour func (mach *OlmMachine) unwedgeDevice(log zerolog.Logger, sender id.UserID, senderKey id.SenderKey) { log = log.With().Str("action", "unwedge olm session").Logger() - ctx := log.WithContext(context.Background()) + ctx := log.WithContext(context.TODO()) mach.recentlyUnwedgedLock.Lock() prevUnwedge, ok := mach.recentlyUnwedged[senderKey] delta := time.Now().Sub(prevUnwedge) diff --git a/vendor/maunium.net/go/mautrix/crypto/devicelist.go b/vendor/maunium.net/go/mautrix/crypto/devicelist.go index 7f77925..f5c07cd 100644 --- a/vendor/maunium.net/go/mautrix/crypto/devicelist.go +++ b/vendor/maunium.net/go/mautrix/crypto/devicelist.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -27,9 +27,16 @@ var ( InvalidKeySignature = errors.New("invalid signature on device keys") ) -func (mach *OlmMachine) LoadDevices(user id.UserID) map[id.DeviceID]*id.Device { - // TODO proper context? - return mach.fetchKeys(context.TODO(), []id.UserID{user}, "", true)[user] +func (mach *OlmMachine) LoadDevices(ctx context.Context, user id.UserID) (keys map[id.DeviceID]*id.Device) { + log := zerolog.Ctx(ctx) + + if keys, err := mach.FetchKeys(ctx, []id.UserID{user}, true); err != nil { + log.Err(err).Msg("Failed to load devices") + } else if keys != nil { + return keys[user] + } + + return nil } func (mach *OlmMachine) storeDeviceSelfSignatures(ctx context.Context, userID id.UserID, deviceID id.DeviceID, resp *mautrix.RespQueryKeys) { @@ -53,7 +60,7 @@ func (mach *OlmMachine) storeDeviceSelfSignatures(ctx context.Context, userID id Str("signed_device_id", deviceID.String()). Str("signature", signature). Msg("Verified self-signing signature") - err = mach.CryptoStore.PutSignature(userID, id.Ed25519(signKey), signerUserID, pubKey, signature) + err = mach.CryptoStore.PutSignature(ctx, userID, id.Ed25519(signKey), signerUserID, pubKey, signature) if err != nil { log.Warn().Err(err). Str("signer_user_id", signerUserID.String()). @@ -74,7 +81,7 @@ func (mach *OlmMachine) storeDeviceSelfSignatures(ctx context.Context, userID id } // save signature of device made by its own device signing key if signKey, ok := deviceKeys.Keys[id.DeviceKeyID(signerKey)]; ok { - err := mach.CryptoStore.PutSignature(userID, id.Ed25519(signKey), signerUserID, id.Ed25519(signKey), signature) + err := mach.CryptoStore.PutSignature(ctx, userID, id.Ed25519(signKey), signerUserID, id.Ed25519(signKey), signature) if err != nil { log.Warn().Err(err). Str("signer_user_id", signerUserID.String()). @@ -86,19 +93,16 @@ func (mach *OlmMachine) storeDeviceSelfSignatures(ctx context.Context, userID id } } -func (mach *OlmMachine) fetchKeys(ctx context.Context, users []id.UserID, sinceToken string, includeUntracked bool) (data map[id.UserID]map[id.DeviceID]*id.Device) { - // TODO this function should probably return errors +func (mach *OlmMachine) FetchKeys(ctx context.Context, users []id.UserID, includeUntracked bool) (data map[id.UserID]map[id.DeviceID]*id.Device, err error) { req := &mautrix.ReqQueryKeys{ DeviceKeys: mautrix.DeviceKeysRequest{}, Timeout: 10 * 1000, - Token: sinceToken, } log := mach.machOrContextLog(ctx) if !includeUntracked { - var err error - users, err = mach.CryptoStore.FilterTrackedUsers(users) + users, err = mach.CryptoStore.FilterTrackedUsers(ctx, users) if err != nil { - log.Warn().Err(err).Msg("Failed to filter tracked user list") + return nil, fmt.Errorf("failed to filter tracked user list: %w", err) } } if len(users) == 0 { @@ -108,10 +112,9 @@ func (mach *OlmMachine) fetchKeys(ctx context.Context, users []id.UserID, sinceT req.DeviceKeys[userID] = mautrix.DeviceIDList{} } log.Debug().Strs("users", strishArray(users)).Msg("Querying keys for users") - resp, err := mach.Client.QueryKeys(req) + resp, err := mach.Client.QueryKeys(ctx, req) if err != nil { - log.Error().Err(err).Msg("Failed to query keys") - return + return nil, fmt.Errorf("failed to query keys: %w", err) } for server, err := range resp.Failures { log.Warn().Interface("query_error", err).Str("server", server).Msg("Query keys failure for server") @@ -123,7 +126,7 @@ func (mach *OlmMachine) fetchKeys(ctx context.Context, users []id.UserID, sinceT delete(req.DeviceKeys, userID) newDevices := make(map[id.DeviceID]*id.Device) - existingDevices, err := mach.CryptoStore.GetDevices(userID) + existingDevices, err := mach.CryptoStore.GetDevices(ctx, userID) if err != nil { log.Warn().Err(err).Msg("Failed to get existing devices for user") existingDevices = make(map[id.DeviceID]*id.Device) @@ -151,7 +154,7 @@ func (mach *OlmMachine) fetchKeys(ctx context.Context, users []id.UserID, sinceT } } log.Trace().Int("new_device_count", len(newDevices)).Msg("Storing new device list") - err = mach.CryptoStore.PutDevices(userID, newDevices) + err = mach.CryptoStore.PutDevices(ctx, userID, newDevices) if err != nil { log.Warn().Err(err).Msg("Failed to update device list") } @@ -169,7 +172,7 @@ func (mach *OlmMachine) fetchKeys(ctx context.Context, users []id.UserID, sinceT Str("identity_key", device.IdentityKey.String()). Str("signing_key", device.SigningKey.String()). Logger() - sessionIDs, err := mach.CryptoStore.RedactGroupSessions("", device.IdentityKey, "device removed") + sessionIDs, err := mach.CryptoStore.RedactGroupSessions(ctx, "", device.IdentityKey, "device removed") if err != nil { log.Err(err).Msg("Failed to redact megolm sessions from deleted device") } else { @@ -179,7 +182,7 @@ func (mach *OlmMachine) fetchKeys(ctx context.Context, users []id.UserID, sinceT } } } - mach.OnDevicesChanged(userID) + mach.OnDevicesChanged(ctx, userID) } } for userID := range req.DeviceKeys { @@ -190,25 +193,32 @@ func (mach *OlmMachine) fetchKeys(ctx context.Context, users []id.UserID, sinceT mach.storeCrossSigningKeys(ctx, resp.SelfSigningKeys, resp.DeviceKeys) mach.storeCrossSigningKeys(ctx, resp.UserSigningKeys, resp.DeviceKeys) - return data + return data, nil } // OnDevicesChanged finds all shared rooms with the given user and invalidates outbound sessions in those rooms. // // This is called automatically whenever a device list change is noticed in ProcessSyncResponse and usually does // not need to be called manually. -func (mach *OlmMachine) OnDevicesChanged(userID id.UserID) { +func (mach *OlmMachine) OnDevicesChanged(ctx context.Context, userID id.UserID) { if mach.DisableDeviceChangeKeyRotation { return } - for _, roomID := range mach.StateStore.FindSharedRooms(userID) { - mach.Log.Debug(). + rooms, err := mach.StateStore.FindSharedRooms(ctx, userID) + if err != nil { + mach.machOrContextLog(ctx).Err(err). + Stringer("with_user_id", userID). + Msg("Failed to find shared rooms to invalidate group sessions") + return + } + for _, roomID := range rooms { + mach.machOrContextLog(ctx).Debug(). Str("user_id", userID.String()). Str("room_id", roomID.String()). Msg("Invalidating group session in room due to device change notification") - err := mach.CryptoStore.RemoveOutboundGroupSession(roomID) + err = mach.CryptoStore.RemoveOutboundGroupSession(ctx, roomID) if err != nil { - mach.Log.Warn().Err(err). + mach.machOrContextLog(ctx).Err(err). Str("user_id", userID.String()). Str("room_id", roomID.String()). Msg("Failed to invalidate outbound group session") diff --git a/vendor/maunium.net/go/mautrix/crypto/encryptmegolm.go b/vendor/maunium.net/go/mautrix/crypto/encryptmegolm.go index 80aef71..dcd36dc 100644 --- a/vendor/maunium.net/go/mautrix/crypto/encryptmegolm.go +++ b/vendor/maunium.net/go/mautrix/crypto/encryptmegolm.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -84,7 +84,7 @@ func parseMessageIndex(ciphertext []byte) (uint, error) { func (mach *OlmMachine) EncryptMegolmEvent(ctx context.Context, roomID id.RoomID, evtType event.Type, content interface{}) (*event.EncryptedEventContent, error) { mach.megolmEncryptLock.Lock() defer mach.megolmEncryptLock.Unlock() - session, err := mach.CryptoStore.GetOutboundGroupSession(roomID) + session, err := mach.CryptoStore.GetOutboundGroupSession(ctx, roomID) if err != nil { return nil, fmt.Errorf("failed to get outbound group session: %w", err) } else if session == nil { @@ -116,7 +116,7 @@ func (mach *OlmMachine) EncryptMegolmEvent(ctx context.Context, roomID id.RoomID log = log.With().Uint("message_index", idx).Logger() } log.Debug().Msg("Encrypted event successfully") - err = mach.CryptoStore.UpdateOutboundGroupSession(session) + err = mach.CryptoStore.UpdateOutboundGroupSession(ctx, session) if err != nil { log.Warn().Err(err).Msg("Failed to update megolm session in crypto store after encrypting") } @@ -137,7 +137,13 @@ func (mach *OlmMachine) EncryptMegolmEvent(ctx context.Context, roomID id.RoomID } func (mach *OlmMachine) newOutboundGroupSession(ctx context.Context, roomID id.RoomID) *OutboundGroupSession { - session := NewOutboundGroupSession(roomID, mach.StateStore.GetEncryptionEvent(roomID)) + encryptionEvent, err := mach.StateStore.GetEncryptionEvent(ctx, roomID) + if err != nil { + mach.machOrContextLog(ctx).Err(err). + Stringer("room_id", roomID). + Msg("Failed to get encryption event in room") + } + session := NewOutboundGroupSession(roomID, encryptionEvent) if !mach.DontStoreOutboundKeys { signingKey, idKey := mach.account.Keys() mach.createGroupSession(ctx, idKey, signingKey, roomID, session.ID(), session.Internal.Key(), session.MaxAge, session.MaxMessages, false) @@ -165,7 +171,7 @@ func strishArray[T ~string](arr []T) []string { func (mach *OlmMachine) ShareGroupSession(ctx context.Context, roomID id.RoomID, users []id.UserID) error { mach.megolmEncryptLock.Lock() defer mach.megolmEncryptLock.Unlock() - session, err := mach.CryptoStore.GetOutboundGroupSession(roomID) + session, err := mach.CryptoStore.GetOutboundGroupSession(ctx, roomID) if err != nil { return fmt.Errorf("failed to get previous outbound group session: %w", err) } else if session != nil && session.Shared && !session.Expired() { @@ -192,7 +198,7 @@ func (mach *OlmMachine) ShareGroupSession(ctx context.Context, roomID id.RoomID, for _, userID := range users { log := log.With().Str("target_user_id", userID.String()).Logger() - devices, err := mach.CryptoStore.GetDevices(userID) + devices, err := mach.CryptoStore.GetDevices(ctx, userID) if err != nil { log.Error().Err(err).Msg("Failed to get devices of user") } else if devices == nil { @@ -223,12 +229,16 @@ func (mach *OlmMachine) ShareGroupSession(ctx context.Context, roomID id.RoomID, if len(fetchKeys) > 0 { log.Debug().Strs("users", strishArray(fetchKeys)).Msg("Fetching missing keys") - for userID, devices := range mach.fetchKeys(ctx, fetchKeys, "", true) { - log.Debug(). - Int("device_count", len(devices)). - Str("target_user_id", userID.String()). - Msg("Got device keys for user") - missingSessions[userID] = devices + if keys, err := mach.FetchKeys(ctx, fetchKeys, true); err != nil { + log.Err(err).Strs("users", strishArray(fetchKeys)).Msg("Failed to fetch missing keys") + } else if keys != nil { + for userID, devices := range keys { + log.Debug(). + Int("device_count", len(devices)). + Str("target_user_id", userID.String()). + Msg("Got device keys for user") + missingSessions[userID] = devices + } } } @@ -280,11 +290,11 @@ func (mach *OlmMachine) ShareGroupSession(ctx context.Context, roomID id.RoomID, Int("user_count", len(toDeviceWithheld.Messages)). Msg("Sending to-device messages to report withheld key") // TODO remove the next 4 lines once clients support m.room_key.withheld - _, err = mach.Client.SendToDevice(event.ToDeviceOrgMatrixRoomKeyWithheld, toDeviceWithheld) + _, err = mach.Client.SendToDevice(ctx, event.ToDeviceOrgMatrixRoomKeyWithheld, toDeviceWithheld) if err != nil { log.Warn().Err(err).Msg("Failed to report withheld keys (legacy event type)") } - _, err = mach.Client.SendToDevice(event.ToDeviceRoomKeyWithheld, toDeviceWithheld) + _, err = mach.Client.SendToDevice(ctx, event.ToDeviceRoomKeyWithheld, toDeviceWithheld) if err != nil { log.Warn().Err(err).Msg("Failed to report withheld keys") } @@ -292,7 +302,7 @@ func (mach *OlmMachine) ShareGroupSession(ctx context.Context, roomID id.RoomID, log.Debug().Msg("Group session successfully shared") session.Shared = true - return mach.CryptoStore.AddOutboundGroupSession(session) + return mach.CryptoStore.AddOutboundGroupSession(ctx, session) } func (mach *OlmMachine) encryptAndSendGroupSession(ctx context.Context, session *OutboundGroupSession, olmSessions map[id.UserID]map[id.DeviceID]deviceSessionWrapper) error { @@ -327,7 +337,7 @@ func (mach *OlmMachine) encryptAndSendGroupSession(ctx context.Context, session Int("device_count", deviceCount). Int("user_count", len(toDevice.Messages)). Msg("Sending to-device messages to share group session") - _, err := mach.Client.SendToDevice(event.ToDeviceEncrypted, toDevice) + _, err := mach.Client.SendToDevice(ctx, event.ToDeviceEncrypted, toDevice) return err } @@ -367,7 +377,7 @@ func (mach *OlmMachine) findOlmSessionsForUser(ctx context.Context, session *Out Reason: "This device does not encrypt messages for unverified devices", }} session.Users[userKey] = OGSIgnored - } else if deviceSession, err := mach.CryptoStore.GetLatestSession(device.IdentityKey); err != nil { + } else if deviceSession, err := mach.CryptoStore.GetLatestSession(ctx, device.IdentityKey); err != nil { log.Error().Err(err).Msg("Failed to get olm session to encrypt group session") } else if deviceSession == nil { log.Warn().Err(err).Msg("Didn't find olm session to encrypt group session") diff --git a/vendor/maunium.net/go/mautrix/crypto/encryptolm.go b/vendor/maunium.net/go/mautrix/crypto/encryptolm.go index 8ae3ba1..3b1d40d 100644 --- a/vendor/maunium.net/go/mautrix/crypto/encryptolm.go +++ b/vendor/maunium.net/go/mautrix/crypto/encryptolm.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -38,7 +38,7 @@ func (mach *OlmMachine) encryptOlmEvent(ctx context.Context, session *OlmSession Str("olm_session_description", session.Describe()). Msg("Encrypting olm message") msgType, ciphertext := session.Encrypt(plaintext) - err = mach.CryptoStore.UpdateSession(recipient.IdentityKey, session) + err = mach.CryptoStore.UpdateSession(ctx, recipient.IdentityKey, session) if err != nil { log.Error().Err(err).Msg("Failed to update olm session in crypto store after encrypting") } @@ -54,8 +54,8 @@ func (mach *OlmMachine) encryptOlmEvent(ctx context.Context, session *OlmSession } } -func (mach *OlmMachine) shouldCreateNewSession(identityKey id.IdentityKey) bool { - if !mach.CryptoStore.HasSession(identityKey) { +func (mach *OlmMachine) shouldCreateNewSession(ctx context.Context, identityKey id.IdentityKey) bool { + if !mach.CryptoStore.HasSession(ctx, identityKey) { return true } mach.devicesToUnwedgeLock.Lock() @@ -72,7 +72,7 @@ func (mach *OlmMachine) createOutboundSessions(ctx context.Context, input map[id for userID, devices := range input { request[userID] = make(map[id.DeviceID]id.KeyAlgorithm) for deviceID, identity := range devices { - if mach.shouldCreateNewSession(identity.IdentityKey) { + if mach.shouldCreateNewSession(ctx, identity.IdentityKey) { request[userID][deviceID] = id.KeyAlgorithmSignedCurve25519 } } @@ -83,7 +83,7 @@ func (mach *OlmMachine) createOutboundSessions(ctx context.Context, input map[id if len(request) == 0 { return nil } - resp, err := mach.Client.ClaimKeys(&mautrix.ReqClaimKeys{ + resp, err := mach.Client.ClaimKeys(ctx, &mautrix.ReqClaimKeys{ OneTimeKeys: request, Timeout: 10 * 1000, }) @@ -117,7 +117,7 @@ func (mach *OlmMachine) createOutboundSessions(ctx context.Context, input map[id log.Error().Err(err).Msg("Failed to create outbound session with claimed one-time key") } else { wrapped := wrapSession(sess) - err = mach.CryptoStore.AddSession(identity.IdentityKey, wrapped) + err = mach.CryptoStore.AddSession(ctx, identity.IdentityKey, wrapped) if err != nil { log.Error().Err(err).Msg("Failed to store created outbound session") } else { diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/README.md b/vendor/maunium.net/go/mautrix/crypto/goolm/README.md new file mode 100644 index 0000000..c5eaa0a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/README.md @@ -0,0 +1,5 @@ +# go-olm +This is a fork of [DerLukas's goolm](https://codeberg.org/DerLukas/goolm), +a pure Go implementation of libolm. + +The original project is licensed under the MIT license. diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/account/account.go b/vendor/maunium.net/go/mautrix/crypto/goolm/account/account.go new file mode 100644 index 0000000..7896f84 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/account/account.go @@ -0,0 +1,522 @@ +// account packages an account which stores the identity, one time keys and fallback keys. +package account + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + + "maunium.net/go/mautrix/id" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/goolm/session" + "maunium.net/go/mautrix/crypto/goolm/utilities" +) + +const ( + accountPickleVersionJSON byte = 1 + accountPickleVersionLibOLM uint32 = 4 + MaxOneTimeKeys int = 100 //maximum number of stored one time keys per Account +) + +// Account stores an account for end to end encrypted messaging via the olm protocol. +// An Account can not be used to en/decrypt messages. However it can be used to contruct new olm sessions, which in turn do the en/decryption. +// There is no tracking of sessions in an account. +type Account struct { + IdKeys struct { + Ed25519 crypto.Ed25519KeyPair `json:"ed25519,omitempty"` + Curve25519 crypto.Curve25519KeyPair `json:"curve25519,omitempty"` + } `json:"identity_keys"` + OTKeys []crypto.OneTimeKey `json:"one_time_keys"` + CurrentFallbackKey crypto.OneTimeKey `json:"current_fallback_key,omitempty"` + PrevFallbackKey crypto.OneTimeKey `json:"prev_fallback_key,omitempty"` + NextOneTimeKeyID uint32 `json:"next_one_time_key_id,omitempty"` + NumFallbackKeys uint8 `json:"number_fallback_keys"` +} + +// AccountFromJSONPickled loads the Account details from a pickled base64 string. The input is decrypted with the supplied key. +func AccountFromJSONPickled(pickled, key []byte) (*Account, error) { + if len(pickled) == 0 { + return nil, fmt.Errorf("accountFromPickled: %w", goolm.ErrEmptyInput) + } + a := &Account{} + err := a.UnpickleAsJSON(pickled, key) + if err != nil { + return nil, err + } + return a, nil +} + +// AccountFromPickled loads the Account details from a pickled base64 string. The input is decrypted with the supplied key. +func AccountFromPickled(pickled, key []byte) (*Account, error) { + if len(pickled) == 0 { + return nil, fmt.Errorf("accountFromPickled: %w", goolm.ErrEmptyInput) + } + a := &Account{} + err := a.Unpickle(pickled, key) + if err != nil { + return nil, err + } + return a, nil +} + +// NewAccount creates a new Account. If reader is nil, crypto/rand is used for the key creation. +func NewAccount(reader io.Reader) (*Account, error) { + a := &Account{} + kPEd25519, err := crypto.Ed25519GenerateKey(reader) + if err != nil { + return nil, err + } + a.IdKeys.Ed25519 = kPEd25519 + kPCurve25519, err := crypto.Curve25519GenerateKey(reader) + if err != nil { + return nil, err + } + a.IdKeys.Curve25519 = kPCurve25519 + return a, nil +} + +// PickleAsJSON returns an Account as a base64 string encrypted using the supplied key. The unencrypted representation of the Account is in JSON format. +func (a Account) PickleAsJSON(key []byte) ([]byte, error) { + return utilities.PickleAsJSON(a, accountPickleVersionJSON, key) +} + +// UnpickleAsJSON updates an Account by a base64 encrypted string using the supplied key. The unencrypted representation has to be in JSON format. +func (a *Account) UnpickleAsJSON(pickled, key []byte) error { + return utilities.UnpickleAsJSON(a, pickled, key, accountPickleVersionJSON) +} + +// IdentityKeysJSON returns the public parts of the identity keys for the Account in a JSON string. +func (a Account) IdentityKeysJSON() ([]byte, error) { + res := struct { + Ed25519 string `json:"ed25519"` + Curve25519 string `json:"curve25519"` + }{} + ed25519, curve25519 := a.IdentityKeys() + res.Ed25519 = string(ed25519) + res.Curve25519 = string(curve25519) + return json.Marshal(res) +} + +// IdentityKeys returns the public parts of the Ed25519 and Curve25519 identity keys for the Account. +func (a Account) IdentityKeys() (id.Ed25519, id.Curve25519) { + ed25519 := id.Ed25519(base64.RawStdEncoding.EncodeToString(a.IdKeys.Ed25519.PublicKey)) + curve25519 := id.Curve25519(base64.RawStdEncoding.EncodeToString(a.IdKeys.Curve25519.PublicKey)) + return ed25519, curve25519 +} + +// Sign returns the signature of a message using the Ed25519 key for this Account. +func (a Account) Sign(message []byte) ([]byte, error) { + if len(message) == 0 { + return nil, fmt.Errorf("sign: %w", goolm.ErrEmptyInput) + } + return goolm.Base64Encode(a.IdKeys.Ed25519.Sign(message)), nil +} + +// OneTimeKeys returns the public parts of the unpublished one time keys of the Account. +// +// The returned data is a map with the mapping of key id to base64-encoded Curve25519 key. +func (a Account) OneTimeKeys() map[string]id.Curve25519 { + oneTimeKeys := make(map[string]id.Curve25519) + for _, curKey := range a.OTKeys { + if !curKey.Published { + oneTimeKeys[curKey.KeyIDEncoded()] = id.Curve25519(curKey.PublicKeyEncoded()) + } + } + return oneTimeKeys +} + +//OneTimeKeysJSON returns the public parts of the unpublished one time keys of the Account as a JSON string. +// +//The returned JSON is of format: +/* + { + Curve25519: { + "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo", + "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU" + } + } +*/ +func (a Account) OneTimeKeysJSON() ([]byte, error) { + res := make(map[string]map[string]id.Curve25519) + otKeys := a.OneTimeKeys() + res["Curve25519"] = otKeys + return json.Marshal(res) +} + +// MarkKeysAsPublished marks the current set of one time keys and the fallback key as being +// published. +func (a *Account) MarkKeysAsPublished() { + for keyIndex := range a.OTKeys { + if !a.OTKeys[keyIndex].Published { + a.OTKeys[keyIndex].Published = true + } + } + a.CurrentFallbackKey.Published = true +} + +// GenOneTimeKeys generates a number of new one time keys. If the total number +// of keys stored by this Account exceeds MaxOneTimeKeys then the older +// keys are discarded. If reader is nil, crypto/rand is used for the key creation. +func (a *Account) GenOneTimeKeys(reader io.Reader, num uint) error { + for i := uint(0); i < num; i++ { + key := crypto.OneTimeKey{ + Published: false, + ID: a.NextOneTimeKeyID, + } + newKP, err := crypto.Curve25519GenerateKey(reader) + if err != nil { + return err + } + key.Key = newKP + a.NextOneTimeKeyID++ + a.OTKeys = append([]crypto.OneTimeKey{key}, a.OTKeys...) + } + if len(a.OTKeys) > MaxOneTimeKeys { + a.OTKeys = a.OTKeys[:MaxOneTimeKeys] + } + return nil +} + +// NewOutboundSession creates a new outbound session to a +// given curve25519 identity Key and one time key. +func (a Account) NewOutboundSession(theirIdentityKey, theirOneTimeKey id.Curve25519) (*session.OlmSession, error) { + if len(theirIdentityKey) == 0 || len(theirOneTimeKey) == 0 { + return nil, fmt.Errorf("outbound session: %w", goolm.ErrEmptyInput) + } + theirIdentityKeyDecoded, err := base64.RawStdEncoding.DecodeString(string(theirIdentityKey)) + if err != nil { + return nil, err + } + theirOneTimeKeyDecoded, err := base64.RawStdEncoding.DecodeString(string(theirOneTimeKey)) + if err != nil { + return nil, err + } + s, err := session.NewOutboundOlmSession(a.IdKeys.Curve25519, theirIdentityKeyDecoded, theirOneTimeKeyDecoded) + if err != nil { + return nil, err + } + return s, nil +} + +// NewInboundSession creates a new inbound session from an incoming PRE_KEY message. +func (a Account) NewInboundSession(theirIdentityKey *id.Curve25519, oneTimeKeyMsg []byte) (*session.OlmSession, error) { + if len(oneTimeKeyMsg) == 0 { + return nil, fmt.Errorf("inbound session: %w", goolm.ErrEmptyInput) + } + var theirIdentityKeyDecoded *crypto.Curve25519PublicKey + var err error + if theirIdentityKey != nil { + theirIdentityKeyDecodedByte, err := base64.RawStdEncoding.DecodeString(string(*theirIdentityKey)) + if err != nil { + return nil, err + } + theirIdentityKeyCurve := crypto.Curve25519PublicKey(theirIdentityKeyDecodedByte) + theirIdentityKeyDecoded = &theirIdentityKeyCurve + } + + s, err := session.NewInboundOlmSession(theirIdentityKeyDecoded, oneTimeKeyMsg, a.searchOTKForOur, a.IdKeys.Curve25519) + if err != nil { + return nil, err + } + return s, nil +} + +func (a Account) searchOTKForOur(toFind crypto.Curve25519PublicKey) *crypto.OneTimeKey { + for curIndex := range a.OTKeys { + if a.OTKeys[curIndex].Key.PublicKey.Equal(toFind) { + return &a.OTKeys[curIndex] + } + } + if a.NumFallbackKeys >= 1 && a.CurrentFallbackKey.Key.PublicKey.Equal(toFind) { + return &a.CurrentFallbackKey + } + if a.NumFallbackKeys >= 2 && a.PrevFallbackKey.Key.PublicKey.Equal(toFind) { + return &a.PrevFallbackKey + } + return nil +} + +// RemoveOneTimeKeys removes the one time key in this Account which matches the one time key in the session s. +func (a *Account) RemoveOneTimeKeys(s *session.OlmSession) { + toFind := s.BobOneTimeKey + for curIndex := range a.OTKeys { + if a.OTKeys[curIndex].Key.PublicKey.Equal(toFind) { + //Remove and return + a.OTKeys[curIndex] = a.OTKeys[len(a.OTKeys)-1] + a.OTKeys = a.OTKeys[:len(a.OTKeys)-1] + return + } + } + //if the key is a fallback or prevFallback, don't remove it +} + +// GenFallbackKey generates a new fallback key. The old fallback key is stored in a.PrevFallbackKey overwriting any previous PrevFallbackKey. If reader is nil, crypto/rand is used for the key creation. +func (a *Account) GenFallbackKey(reader io.Reader) error { + a.PrevFallbackKey = a.CurrentFallbackKey + key := crypto.OneTimeKey{ + Published: false, + ID: a.NextOneTimeKeyID, + } + newKP, err := crypto.Curve25519GenerateKey(reader) + if err != nil { + return err + } + key.Key = newKP + a.NextOneTimeKeyID++ + if a.NumFallbackKeys < 2 { + a.NumFallbackKeys++ + } + a.CurrentFallbackKey = key + return nil +} + +// FallbackKey returns the public part of the current fallback key of the Account. +// The returned data is a map with the mapping of key id to base64-encoded Curve25519 key. +func (a Account) FallbackKey() map[string]id.Curve25519 { + keys := make(map[string]id.Curve25519) + if a.NumFallbackKeys >= 1 { + keys[a.CurrentFallbackKey.KeyIDEncoded()] = id.Curve25519(a.CurrentFallbackKey.PublicKeyEncoded()) + } + return keys +} + +//FallbackKeyJSON returns the public part of the current fallback key of the Account as a JSON string. +// +//The returned JSON is of format: +/* + { + curve25519: { + "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo" + } + } +*/ +func (a Account) FallbackKeyJSON() ([]byte, error) { + res := make(map[string]map[string]id.Curve25519) + fbk := a.FallbackKey() + res["curve25519"] = fbk + return json.Marshal(res) +} + +// FallbackKeyUnpublished returns the public part of the current fallback key of the Account only if it is unpublished. +// The returned data is a map with the mapping of key id to base64-encoded Curve25519 key. +func (a Account) FallbackKeyUnpublished() map[string]id.Curve25519 { + keys := make(map[string]id.Curve25519) + if a.NumFallbackKeys >= 1 && !a.CurrentFallbackKey.Published { + keys[a.CurrentFallbackKey.KeyIDEncoded()] = id.Curve25519(a.CurrentFallbackKey.PublicKeyEncoded()) + } + return keys +} + +//FallbackKeyUnpublishedJSON returns the public part of the current fallback key, only if it is unpublished, of the Account as a JSON string. +// +//The returned JSON is of format: +/* + { + curve25519: { + "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo" + } + } +*/ +func (a Account) FallbackKeyUnpublishedJSON() ([]byte, error) { + res := make(map[string]map[string]id.Curve25519) + fbk := a.FallbackKeyUnpublished() + res["curve25519"] = fbk + return json.Marshal(res) +} + +// ForgetOldFallbackKey resets the previous fallback key in the account. +func (a *Account) ForgetOldFallbackKey() { + if a.NumFallbackKeys >= 2 { + a.NumFallbackKeys = 1 + a.PrevFallbackKey = crypto.OneTimeKey{} + } +} + +// Unpickle decodes the base64 encoded string and decrypts the result with the key. +// The decrypted value is then passed to UnpickleLibOlm. +func (a *Account) Unpickle(pickled, key []byte) error { + decrypted, err := cipher.Unpickle(key, pickled) + if err != nil { + return err + } + _, err = a.UnpickleLibOlm(decrypted) + return err +} + +// UnpickleLibOlm decodes the unencryted value and populates the Account accordingly. It returns the number of bytes read. +func (a *Account) UnpickleLibOlm(value []byte) (int, error) { + //First 4 bytes are the accountPickleVersion + pickledVersion, curPos, err := libolmpickle.UnpickleUInt32(value) + if err != nil { + return 0, err + } + switch pickledVersion { + case accountPickleVersionLibOLM, 3, 2: + default: + return 0, fmt.Errorf("unpickle account: %w", goolm.ErrBadVersion) + } + //read ed25519 key pair + readBytes, err := a.IdKeys.Ed25519.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + //read curve25519 key pair + readBytes, err = a.IdKeys.Curve25519.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + //Read number of onetimeKeys + numberOTKeys, readBytes, err := libolmpickle.UnpickleUInt32(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + //Read i one time keys + a.OTKeys = make([]crypto.OneTimeKey, numberOTKeys) + for i := uint32(0); i < numberOTKeys; i++ { + readBytes, err := a.OTKeys[i].UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + } + if pickledVersion <= 2 { + // version 2 did not have fallback keys + a.NumFallbackKeys = 0 + } else if pickledVersion == 3 { + // version 3 used the published flag to indicate how many fallback keys + // were present (we'll have to assume that the keys were published) + readBytes, err := a.CurrentFallbackKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = a.PrevFallbackKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + if a.CurrentFallbackKey.Published { + if a.PrevFallbackKey.Published { + a.NumFallbackKeys = 2 + } else { + a.NumFallbackKeys = 1 + } + } else { + a.NumFallbackKeys = 0 + } + } else { + //Read number of fallback keys + numFallbackKeys, readBytes, err := libolmpickle.UnpickleUInt8(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + a.NumFallbackKeys = numFallbackKeys + if a.NumFallbackKeys >= 1 { + readBytes, err := a.CurrentFallbackKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + if a.NumFallbackKeys >= 2 { + readBytes, err := a.PrevFallbackKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + } + } + } + //Read next onetime key id + nextOTKeyID, readBytes, err := libolmpickle.UnpickleUInt32(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + a.NextOneTimeKeyID = nextOTKeyID + return curPos, nil +} + +// Pickle returns a base64 encoded and with key encrypted pickled account using PickleLibOlm(). +func (a Account) Pickle(key []byte) ([]byte, error) { + pickeledBytes := make([]byte, a.PickleLen()) + written, err := a.PickleLibOlm(pickeledBytes) + if err != nil { + return nil, err + } + if written != len(pickeledBytes) { + return nil, errors.New("number of written bytes not correct") + } + encrypted, err := cipher.Pickle(key, pickeledBytes) + if err != nil { + return nil, err + } + return encrypted, nil +} + +// PickleLibOlm encodes the Account into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (a Account) PickleLibOlm(target []byte) (int, error) { + if len(target) < a.PickleLen() { + return 0, fmt.Errorf("pickle account: %w", goolm.ErrValueTooShort) + } + written := libolmpickle.PickleUInt32(accountPickleVersionLibOLM, target) + writtenEdKey, err := a.IdKeys.Ed25519.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle account: %w", err) + } + written += writtenEdKey + writtenCurveKey, err := a.IdKeys.Curve25519.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle account: %w", err) + } + written += writtenCurveKey + written += libolmpickle.PickleUInt32(uint32(len(a.OTKeys)), target[written:]) + for _, curOTKey := range a.OTKeys { + writtenOT, err := curOTKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle account: %w", err) + } + written += writtenOT + } + written += libolmpickle.PickleUInt8(a.NumFallbackKeys, target[written:]) + if a.NumFallbackKeys >= 1 { + writtenOT, err := a.CurrentFallbackKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle account: %w", err) + } + written += writtenOT + + if a.NumFallbackKeys >= 2 { + writtenOT, err := a.PrevFallbackKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle account: %w", err) + } + written += writtenOT + } + } + written += libolmpickle.PickleUInt32(a.NextOneTimeKeyID, target[written:]) + return written, nil +} + +// PickleLen returns the number of bytes the pickled Account will have. +func (a Account) PickleLen() int { + length := libolmpickle.PickleUInt32Len(accountPickleVersionLibOLM) + length += a.IdKeys.Ed25519.PickleLen() + length += a.IdKeys.Curve25519.PickleLen() + length += libolmpickle.PickleUInt32Len(uint32(len(a.OTKeys))) + length += (len(a.OTKeys) * (&crypto.OneTimeKey{}).PickleLen()) + length += libolmpickle.PickleUInt8Len(a.NumFallbackKeys) + length += (int(a.NumFallbackKeys) * (&crypto.OneTimeKey{}).PickleLen()) + length += libolmpickle.PickleUInt32Len(a.NextOneTimeKeyID) + return length +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/base64.go b/vendor/maunium.net/go/mautrix/crypto/goolm/base64.go new file mode 100644 index 0000000..229008c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/base64.go @@ -0,0 +1,22 @@ +package goolm + +import ( + "encoding/base64" +) + +// Deprecated: base64.RawStdEncoding should be used directly +func Base64Decode(input []byte) ([]byte, error) { + decoded := make([]byte, base64.RawStdEncoding.DecodedLen(len(input))) + writtenBytes, err := base64.RawStdEncoding.Decode(decoded, input) + if err != nil { + return nil, err + } + return decoded[:writtenBytes], nil +} + +// Deprecated: base64.RawStdEncoding should be used directly +func Base64Encode(input []byte) []byte { + encoded := make([]byte, base64.RawStdEncoding.EncodedLen(len(input))) + base64.RawStdEncoding.Encode(encoded, input) + return encoded +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/aes_sha256.go b/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/aes_sha256.go new file mode 100644 index 0000000..1155949 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/aes_sha256.go @@ -0,0 +1,96 @@ +package cipher + +import ( + "bytes" + "io" + + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +// derivedAESKeys stores the derived keys for the AESSHA256 cipher +type derivedAESKeys struct { + key []byte + hmacKey []byte + iv []byte +} + +// deriveAESKeys derives three keys for the AESSHA256 cipher +func deriveAESKeys(kdfInfo []byte, key []byte) (*derivedAESKeys, error) { + hkdf := crypto.HKDFSHA256(key, nil, kdfInfo) + keys := &derivedAESKeys{ + key: make([]byte, 32), + hmacKey: make([]byte, 32), + iv: make([]byte, 16), + } + if _, err := io.ReadFull(hkdf, keys.key); err != nil { + return nil, err + } + if _, err := io.ReadFull(hkdf, keys.hmacKey); err != nil { + return nil, err + } + if _, err := io.ReadFull(hkdf, keys.iv); err != nil { + return nil, err + } + return keys, nil +} + +// AESSha512BlockSize resturns the blocksize of the cipher AESSHA256. +func AESSha512BlockSize() int { + return crypto.AESCBCBlocksize() +} + +// AESSHA256 is a valid cipher using AES with CBC and HKDFSha256. +type AESSHA256 struct { + kdfInfo []byte +} + +// NewAESSHA256 returns a new AESSHA256 cipher with the key derive function info (kdfInfo). +func NewAESSHA256(kdfInfo []byte) *AESSHA256 { + return &AESSHA256{ + kdfInfo: kdfInfo, + } +} + +// Encrypt encrypts the plaintext with the key. The key is used to derive the actual encryption key (32 bytes) as well as the iv (16 bytes). +func (c AESSHA256) Encrypt(key, plaintext []byte) (ciphertext []byte, err error) { + keys, err := deriveAESKeys(c.kdfInfo, key) + if err != nil { + return nil, err + } + ciphertext, err = crypto.AESCBCEncrypt(keys.key, keys.iv, plaintext) + if err != nil { + return nil, err + } + return ciphertext, nil +} + +// Decrypt decrypts the ciphertext with the key. The key is used to derive the actual encryption key (32 bytes) as well as the iv (16 bytes). +func (c AESSHA256) Decrypt(key, ciphertext []byte) (plaintext []byte, err error) { + keys, err := deriveAESKeys(c.kdfInfo, key) + if err != nil { + return nil, err + } + plaintext, err = crypto.AESCBCDecrypt(keys.key, keys.iv, ciphertext) + if err != nil { + return nil, err + } + return plaintext, nil +} + +// MAC returns the MAC for the message using the key. The key is used to derive the actual mac key (32 bytes). +func (c AESSHA256) MAC(key, message []byte) ([]byte, error) { + keys, err := deriveAESKeys(c.kdfInfo, key) + if err != nil { + return nil, err + } + return crypto.HMACSHA256(keys.hmacKey, message), nil +} + +// Verify checks the MAC of the message using the key against the givenMAC. The key is used to derive the actual mac key (32 bytes). +func (c AESSHA256) Verify(key, message, givenMAC []byte) (bool, error) { + mac, err := c.MAC(key, message) + if err != nil { + return false, err + } + return bytes.Equal(givenMAC, mac[:len(givenMAC)]), nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/main.go b/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/main.go new file mode 100644 index 0000000..a866470 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/main.go @@ -0,0 +1,17 @@ +// cipher provides the methods and structs to do encryptions for olm/megolm. +package cipher + +// Cipher defines a valid cipher. +type Cipher interface { + // Encrypt encrypts the plaintext. + Encrypt(key, plaintext []byte) (ciphertext []byte, err error) + + // Decrypt decrypts the ciphertext. + Decrypt(key, ciphertext []byte) (plaintext []byte, err error) + + //MAC returns the MAC of the message calculated with the key. + MAC(key, message []byte) ([]byte, error) + + //Verify checks the MAC of the message calculated with the key against the givenMAC. + Verify(key, message, givenMAC []byte) (bool, error) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/pickle.go b/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/pickle.go new file mode 100644 index 0000000..670ff6f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/cipher/pickle.go @@ -0,0 +1,58 @@ +package cipher + +import ( + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" +) + +const ( + kdfPickle = "Pickle" //used to derive the keys for encryption + pickleMACLength = 8 +) + +// PickleBlockSize returns the blocksize of the used cipher. +func PickleBlockSize() int { + return AESSha512BlockSize() +} + +// Pickle encrypts the input with the key and the cipher AESSHA256. The result is then encoded in base64. +func Pickle(key, input []byte) ([]byte, error) { + pickleCipher := NewAESSHA256([]byte(kdfPickle)) + ciphertext, err := pickleCipher.Encrypt(key, input) + if err != nil { + return nil, err + } + mac, err := pickleCipher.MAC(key, ciphertext) + if err != nil { + return nil, err + } + ciphertext = append(ciphertext, mac[:pickleMACLength]...) + encoded := goolm.Base64Encode(ciphertext) + return encoded, nil +} + +// Unpickle decodes the input from base64 and decrypts the decoded input with the key and the cipher AESSHA256. +func Unpickle(key, input []byte) ([]byte, error) { + pickleCipher := NewAESSHA256([]byte(kdfPickle)) + ciphertext, err := goolm.Base64Decode(input) + if err != nil { + return nil, err + } + //remove mac and check + verified, err := pickleCipher.Verify(key, ciphertext[:len(ciphertext)-pickleMACLength], ciphertext[len(ciphertext)-pickleMACLength:]) + if err != nil { + return nil, err + } + if !verified { + return nil, fmt.Errorf("decrypt pickle: %w", goolm.ErrBadMAC) + } + //Set to next block size + targetCipherText := make([]byte, int(len(ciphertext)/PickleBlockSize())*PickleBlockSize()) + copy(targetCipherText, ciphertext) + plaintext, err := pickleCipher.Decrypt(key, targetCipherText) + if err != nil { + return nil, err + } + return plaintext, nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/aes_cbc.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/aes_cbc.go new file mode 100644 index 0000000..10434ab --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/aes_cbc.go @@ -0,0 +1,75 @@ +package crypto + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" +) + +// AESCBCBlocksize returns the blocksize of the encryption method +func AESCBCBlocksize() int { + return aes.BlockSize +} + +// AESCBCEncrypt encrypts the plaintext with the key and iv. len(iv) must be equal to the blocksize! +func AESCBCEncrypt(key, iv, plaintext []byte) ([]byte, error) { + if len(key) == 0 { + return nil, fmt.Errorf("AESCBCEncrypt: %w", goolm.ErrNoKeyProvided) + } + if len(iv) != AESCBCBlocksize() { + return nil, fmt.Errorf("iv: %w", goolm.ErrNotBlocksize) + } + var cipherText []byte + plaintext = pkcs5Padding(plaintext, AESCBCBlocksize()) + if len(plaintext)%AESCBCBlocksize() != 0 { + return nil, fmt.Errorf("message: %w", goolm.ErrNotMultipleBlocksize) + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + cipherText = make([]byte, len(plaintext)) + cbc := cipher.NewCBCEncrypter(block, iv) + cbc.CryptBlocks(cipherText, plaintext) + return cipherText, nil +} + +// AESCBCDecrypt decrypts the ciphertext with the key and iv. len(iv) must be equal to the blocksize! +func AESCBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) { + if len(key) == 0 { + return nil, fmt.Errorf("AESCBCEncrypt: %w", goolm.ErrNoKeyProvided) + } + if len(iv) != AESCBCBlocksize() { + return nil, fmt.Errorf("iv: %w", goolm.ErrNotBlocksize) + } + var block cipher.Block + var err error + block, err = aes.NewCipher(key) + if err != nil { + return nil, err + } + if len(ciphertext) < AESCBCBlocksize() { + return nil, fmt.Errorf("ciphertext: %w", goolm.ErrNotMultipleBlocksize) + } + + cbc := cipher.NewCBCDecrypter(block, iv) + cbc.CryptBlocks(ciphertext, ciphertext) + return pkcs5Unpadding(ciphertext), nil +} + +// pkcs5Padding paddes the plaintext to be used in the AESCBC encryption. +func pkcs5Padding(plaintext []byte, blockSize int) []byte { + padding := (blockSize - len(plaintext)%blockSize) + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(plaintext, padtext...) +} + +// pkcs5Unpadding undoes the padding to the plaintext after AESCBC decryption. +func pkcs5Unpadding(plaintext []byte) []byte { + length := len(plaintext) + unpadding := int(plaintext[length-1]) + return plaintext[:(length - unpadding)] +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go new file mode 100644 index 0000000..125e1bf --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go @@ -0,0 +1,186 @@ +package crypto + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + "golang.org/x/crypto/curve25519" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/id" +) + +const ( + Curve25519KeyLength = curve25519.ScalarSize //The length of the private key. + curve25519PubKeyLength = 32 +) + +// Curve25519GenerateKey creates a new curve25519 key pair. If reader is nil, the random data is taken from crypto/rand. +func Curve25519GenerateKey(reader io.Reader) (Curve25519KeyPair, error) { + privateKeyByte := make([]byte, Curve25519KeyLength) + if reader == nil { + _, err := rand.Read(privateKeyByte) + if err != nil { + return Curve25519KeyPair{}, err + } + } else { + _, err := reader.Read(privateKeyByte) + if err != nil { + return Curve25519KeyPair{}, err + } + } + + privateKey := Curve25519PrivateKey(privateKeyByte) + + publicKey, err := privateKey.PubKey() + if err != nil { + return Curve25519KeyPair{}, err + } + return Curve25519KeyPair{ + PrivateKey: Curve25519PrivateKey(privateKey), + PublicKey: Curve25519PublicKey(publicKey), + }, nil +} + +// Curve25519GenerateFromPrivate creates a new curve25519 key pair with the private key given. +func Curve25519GenerateFromPrivate(private Curve25519PrivateKey) (Curve25519KeyPair, error) { + publicKey, err := private.PubKey() + if err != nil { + return Curve25519KeyPair{}, err + } + return Curve25519KeyPair{ + PrivateKey: private, + PublicKey: Curve25519PublicKey(publicKey), + }, nil +} + +// Curve25519KeyPair stores both parts of a curve25519 key. +type Curve25519KeyPair struct { + PrivateKey Curve25519PrivateKey `json:"private,omitempty"` + PublicKey Curve25519PublicKey `json:"public,omitempty"` +} + +// B64Encoded returns a base64 encoded string of the public key. +func (c Curve25519KeyPair) B64Encoded() id.Curve25519 { + return c.PublicKey.B64Encoded() +} + +// SharedSecret returns the shared secret between the key pair and the given public key. +func (c Curve25519KeyPair) SharedSecret(pubKey Curve25519PublicKey) ([]byte, error) { + return c.PrivateKey.SharedSecret(pubKey) +} + +// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c Curve25519KeyPair) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle curve25519 key pair: %w", goolm.ErrValueTooShort) + } + written, err := c.PublicKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle curve25519 key pair: %w", err) + } + if len(c.PrivateKey) != Curve25519KeyLength { + written += libolmpickle.PickleBytes(make([]byte, Curve25519KeyLength), target[written:]) + } else { + written += libolmpickle.PickleBytes(c.PrivateKey, target[written:]) + } + return written, nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the key pair accordingly. It returns the number of bytes read. +func (c *Curve25519KeyPair) UnpickleLibOlm(value []byte) (int, error) { + //unpickle PubKey + read, err := c.PublicKey.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + //unpickle PrivateKey + privKey, readPriv, err := libolmpickle.UnpickleBytes(value[read:], Curve25519KeyLength) + if err != nil { + return read, err + } + c.PrivateKey = privKey + return read + readPriv, nil +} + +// PickleLen returns the number of bytes the pickled key pair will have. +func (c Curve25519KeyPair) PickleLen() int { + lenPublic := c.PublicKey.PickleLen() + var lenPrivate int + if len(c.PrivateKey) != Curve25519KeyLength { + lenPrivate = libolmpickle.PickleBytesLen(make([]byte, Curve25519KeyLength)) + } else { + lenPrivate = libolmpickle.PickleBytesLen(c.PrivateKey) + } + return lenPublic + lenPrivate +} + +// Curve25519PrivateKey represents the private key for curve25519 usage +type Curve25519PrivateKey []byte + +// Equal compares the private key to the given private key. +func (c Curve25519PrivateKey) Equal(x Curve25519PrivateKey) bool { + return bytes.Equal(c, x) +} + +// PubKey returns the public key derived from the private key. +func (c Curve25519PrivateKey) PubKey() (Curve25519PublicKey, error) { + publicKey, err := curve25519.X25519(c, curve25519.Basepoint) + if err != nil { + return nil, err + } + return publicKey, nil +} + +// SharedSecret returns the shared secret between the private key and the given public key. +func (c Curve25519PrivateKey) SharedSecret(pubKey Curve25519PublicKey) ([]byte, error) { + return curve25519.X25519(c, pubKey) +} + +// Curve25519PublicKey represents the public key for curve25519 usage +type Curve25519PublicKey []byte + +// Equal compares the public key to the given public key. +func (c Curve25519PublicKey) Equal(x Curve25519PublicKey) bool { + return bytes.Equal(c, x) +} + +// B64Encoded returns a base64 encoded string of the public key. +func (c Curve25519PublicKey) B64Encoded() id.Curve25519 { + return id.Curve25519(base64.RawStdEncoding.EncodeToString(c)) +} + +// PickleLibOlm encodes the public key into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c Curve25519PublicKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle curve25519 public key: %w", goolm.ErrValueTooShort) + } + if len(c) != curve25519PubKeyLength { + return libolmpickle.PickleBytes(make([]byte, curve25519PubKeyLength), target), nil + } + return libolmpickle.PickleBytes(c, target), nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the public key accordingly. It returns the number of bytes read. +func (c *Curve25519PublicKey) UnpickleLibOlm(value []byte) (int, error) { + unpickled, readBytes, err := libolmpickle.UnpickleBytes(value, curve25519PubKeyLength) + if err != nil { + return 0, err + } + *c = unpickled + return readBytes, nil +} + +// PickleLen returns the number of bytes the pickled public key will have. +func (c Curve25519PublicKey) PickleLen() int { + if len(c) != curve25519PubKeyLength { + return libolmpickle.PickleBytesLen(make([]byte, curve25519PubKeyLength)) + } + return libolmpickle.PickleBytesLen(c) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go new file mode 100644 index 0000000..bc21300 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go @@ -0,0 +1,181 @@ +package crypto + +import ( + "bytes" + "crypto/ed25519" + "encoding/base64" + "fmt" + "io" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/id" +) + +const ( + ED25519SignatureSize = ed25519.SignatureSize //The length of a signature +) + +// Ed25519GenerateKey creates a new ed25519 key pair. If reader is nil, the random data is taken from crypto/rand. +func Ed25519GenerateKey(reader io.Reader) (Ed25519KeyPair, error) { + publicKey, privateKey, err := ed25519.GenerateKey(reader) + if err != nil { + return Ed25519KeyPair{}, err + } + return Ed25519KeyPair{ + PrivateKey: Ed25519PrivateKey(privateKey), + PublicKey: Ed25519PublicKey(publicKey), + }, nil +} + +// Ed25519GenerateFromPrivate creates a new ed25519 key pair with the private key given. +func Ed25519GenerateFromPrivate(privKey Ed25519PrivateKey) Ed25519KeyPair { + return Ed25519KeyPair{ + PrivateKey: privKey, + PublicKey: privKey.PubKey(), + } +} + +// Ed25519GenerateFromSeed creates a new ed25519 key pair with a given seed. +func Ed25519GenerateFromSeed(seed []byte) Ed25519KeyPair { + privKey := Ed25519PrivateKey(ed25519.NewKeyFromSeed(seed)) + return Ed25519KeyPair{ + PrivateKey: privKey, + PublicKey: privKey.PubKey(), + } +} + +// Ed25519KeyPair stores both parts of a ed25519 key. +type Ed25519KeyPair struct { + PrivateKey Ed25519PrivateKey `json:"private,omitempty"` + PublicKey Ed25519PublicKey `json:"public,omitempty"` +} + +// B64Encoded returns a base64 encoded string of the public key. +func (c Ed25519KeyPair) B64Encoded() id.Ed25519 { + return id.Ed25519(base64.RawStdEncoding.EncodeToString(c.PublicKey)) +} + +// Sign returns the signature for the message. +func (c Ed25519KeyPair) Sign(message []byte) []byte { + return c.PrivateKey.Sign(message) +} + +// Verify checks the signature of the message against the givenSignature +func (c Ed25519KeyPair) Verify(message, givenSignature []byte) bool { + return c.PublicKey.Verify(message, givenSignature) +} + +// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c Ed25519KeyPair) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle ed25519 key pair: %w", goolm.ErrValueTooShort) + } + written, err := c.PublicKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle ed25519 key pair: %w", err) + } + + if len(c.PrivateKey) != ed25519.PrivateKeySize { + written += libolmpickle.PickleBytes(make([]byte, ed25519.PrivateKeySize), target[written:]) + } else { + written += libolmpickle.PickleBytes(c.PrivateKey, target[written:]) + } + return written, nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the key pair accordingly. It returns the number of bytes read. +func (c *Ed25519KeyPair) UnpickleLibOlm(value []byte) (int, error) { + //unpickle PubKey + read, err := c.PublicKey.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + //unpickle PrivateKey + privKey, readPriv, err := libolmpickle.UnpickleBytes(value[read:], ed25519.PrivateKeySize) + if err != nil { + return read, err + } + c.PrivateKey = privKey + return read + readPriv, nil +} + +// PickleLen returns the number of bytes the pickled key pair will have. +func (c Ed25519KeyPair) PickleLen() int { + lenPublic := c.PublicKey.PickleLen() + var lenPrivate int + if len(c.PrivateKey) != ed25519.PrivateKeySize { + lenPrivate = libolmpickle.PickleBytesLen(make([]byte, ed25519.PrivateKeySize)) + } else { + lenPrivate = libolmpickle.PickleBytesLen(c.PrivateKey) + } + return lenPublic + lenPrivate +} + +// Curve25519PrivateKey represents the private key for ed25519 usage. This is just a wrapper. +type Ed25519PrivateKey ed25519.PrivateKey + +// Equal compares the private key to the given private key. +func (c Ed25519PrivateKey) Equal(x Ed25519PrivateKey) bool { + return bytes.Equal(c, x) +} + +// PubKey returns the public key derived from the private key. +func (c Ed25519PrivateKey) PubKey() Ed25519PublicKey { + publicKey := ed25519.PrivateKey(c).Public() + return Ed25519PublicKey(publicKey.(ed25519.PublicKey)) +} + +// Sign returns the signature for the message. +func (c Ed25519PrivateKey) Sign(message []byte) []byte { + return ed25519.Sign(ed25519.PrivateKey(c), message) +} + +// Ed25519PublicKey represents the public key for ed25519 usage. This is just a wrapper. +type Ed25519PublicKey ed25519.PublicKey + +// Equal compares the public key to the given public key. +func (c Ed25519PublicKey) Equal(x Ed25519PublicKey) bool { + return bytes.Equal(c, x) +} + +// B64Encoded returns a base64 encoded string of the public key. +func (c Ed25519PublicKey) B64Encoded() id.Curve25519 { + return id.Curve25519(base64.RawStdEncoding.EncodeToString(c)) +} + +// Verify checks the signature of the message against the givenSignature +func (c Ed25519PublicKey) Verify(message, givenSignature []byte) bool { + return ed25519.Verify(ed25519.PublicKey(c), message, givenSignature) +} + +// PickleLibOlm encodes the public key into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c Ed25519PublicKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle ed25519 public key: %w", goolm.ErrValueTooShort) + } + if len(c) != ed25519.PublicKeySize { + return libolmpickle.PickleBytes(make([]byte, ed25519.PublicKeySize), target), nil + } + return libolmpickle.PickleBytes(c, target), nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the public key accordingly. It returns the number of bytes read. +func (c *Ed25519PublicKey) UnpickleLibOlm(value []byte) (int, error) { + unpickled, readBytes, err := libolmpickle.UnpickleBytes(value, ed25519.PublicKeySize) + if err != nil { + return 0, err + } + *c = unpickled + return readBytes, nil +} + +// PickleLen returns the number of bytes the pickled public key will have. +func (c Ed25519PublicKey) PickleLen() int { + if len(c) != ed25519.PublicKeySize { + return libolmpickle.PickleBytesLen(make([]byte, ed25519.PublicKeySize)) + } + return libolmpickle.PickleBytesLen(c) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go new file mode 100644 index 0000000..8542f7c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go @@ -0,0 +1,29 @@ +package crypto + +import ( + "crypto/hmac" + "crypto/sha256" + "io" + + "golang.org/x/crypto/hkdf" +) + +// HMACSHA256 returns the hash message authentication code with SHA-256 of the input with the key. +func HMACSHA256(key, input []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(input) + return hash.Sum(nil) +} + +// SHA256 return the SHA-256 of the value. +func SHA256(value []byte) []byte { + hash := sha256.New() + hash.Write(value) + return hash.Sum(nil) +} + +// HKDFSHA256 is the key deivation function based on HMAC and returns a reader based on input. salt and info can both be nil. +// The reader can be used to read an arbitary length of bytes which are based on all parameters. +func HKDFSHA256(input, salt, info []byte) io.Reader { + return hkdf.New(sha256.New, input, salt, info) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/main.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/main.go new file mode 100644 index 0000000..509d44a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/main.go @@ -0,0 +1,2 @@ +// crpyto provides the nessesary encryption methods for olm/megolm +package crypto diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go new file mode 100644 index 0000000..6746556 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go @@ -0,0 +1,95 @@ +package crypto + +import ( + "encoding/base64" + "encoding/binary" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/id" +) + +// OneTimeKey stores the information about a one time key. +type OneTimeKey struct { + ID uint32 `json:"id"` + Published bool `json:"published"` + Key Curve25519KeyPair `json:"key,omitempty"` +} + +// Equal compares the one time key to the given one. +func (otk OneTimeKey) Equal(s OneTimeKey) bool { + if otk.ID != s.ID { + return false + } + if otk.Published != s.Published { + return false + } + if !otk.Key.PrivateKey.Equal(s.Key.PrivateKey) { + return false + } + if !otk.Key.PublicKey.Equal(s.Key.PublicKey) { + return false + } + return true +} + +// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (c OneTimeKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < c.PickleLen() { + return 0, fmt.Errorf("pickle one time key: %w", goolm.ErrValueTooShort) + } + written := libolmpickle.PickleUInt32(uint32(c.ID), target) + written += libolmpickle.PickleBool(c.Published, target[written:]) + writtenKey, err := c.Key.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle one time key: %w", err) + } + written += writtenKey + return written, nil +} + +// UnpickleLibOlm decodes the unencryted value and populates the OneTimeKey accordingly. It returns the number of bytes read. +func (c *OneTimeKey) UnpickleLibOlm(value []byte) (int, error) { + totalReadBytes := 0 + id, readBytes, err := libolmpickle.UnpickleUInt32(value) + if err != nil { + return 0, err + } + totalReadBytes += readBytes + c.ID = id + published, readBytes, err := libolmpickle.UnpickleBool(value[totalReadBytes:]) + if err != nil { + return 0, err + } + totalReadBytes += readBytes + c.Published = published + readBytes, err = c.Key.UnpickleLibOlm(value[totalReadBytes:]) + if err != nil { + return 0, err + } + totalReadBytes += readBytes + return totalReadBytes, nil +} + +// PickleLen returns the number of bytes the pickled OneTimeKey will have. +func (c OneTimeKey) PickleLen() int { + length := 0 + length += libolmpickle.PickleUInt32Len(c.ID) + length += libolmpickle.PickleBoolLen(c.Published) + length += c.Key.PickleLen() + return length +} + +// KeyIDEncoded returns the base64 encoded id. +func (c OneTimeKey) KeyIDEncoded() string { + resSlice := make([]byte, 4) + binary.BigEndian.PutUint32(resSlice, c.ID) + return base64.RawStdEncoding.EncodeToString(resSlice) +} + +// PublicKeyEncoded returns the base64 encoded public key +func (c OneTimeKey) PublicKeyEncoded() id.Curve25519 { + return c.Key.PublicKey.B64Encoded() +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/errors.go b/vendor/maunium.net/go/mautrix/crypto/goolm/errors.go new file mode 100644 index 0000000..4dec984 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/errors.go @@ -0,0 +1,30 @@ +package goolm + +import ( + "errors" +) + +// Those are the most common used errors +var ( + ErrBadSignature = errors.New("bad signature") + ErrBadMAC = errors.New("bad mac") + ErrBadMessageFormat = errors.New("bad message format") + ErrBadVerification = errors.New("bad verification") + ErrWrongProtocolVersion = errors.New("wrong protocol version") + ErrEmptyInput = errors.New("empty input") + ErrNoKeyProvided = errors.New("no key") + ErrBadMessageKeyID = errors.New("bad message key id") + ErrRatchetNotAvailable = errors.New("ratchet not available: attempt to decode a message whose index is earlier than our earliest known session key") + ErrMsgIndexTooHigh = errors.New("message index too high") + ErrProtocolViolation = errors.New("not protocol message order") + ErrMessageKeyNotFound = errors.New("message key not found") + ErrChainTooHigh = errors.New("chain index too high") + ErrBadInput = errors.New("bad input") + ErrBadVersion = errors.New("wrong version") + ErrNotBlocksize = errors.New("length != blocksize") + ErrNotMultipleBlocksize = errors.New("length not a multiple of the blocksize") + ErrWrongPickleVersion = errors.New("wrong pickle version") + ErrValueTooShort = errors.New("value too short") + ErrInputToSmall = errors.New("input too small (truncated?)") + ErrOverflow = errors.New("overflow") +) diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go new file mode 100644 index 0000000..ec125a3 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go @@ -0,0 +1,41 @@ +package libolmpickle + +import ( + "encoding/binary" +) + +func PickleUInt8(value uint8, target []byte) int { + target[0] = value + return 1 +} +func PickleUInt8Len(value uint8) int { + return 1 +} + +func PickleBool(value bool, target []byte) int { + if value { + target[0] = 0x01 + } else { + target[0] = 0x00 + } + return 1 +} +func PickleBoolLen(value bool) int { + return 1 +} + +func PickleBytes(value, target []byte) int { + return copy(target, value) +} +func PickleBytesLen(value []byte) int { + return len(value) +} + +func PickleUInt32(value uint32, target []byte) int { + res := make([]byte, 4) //4 bytes for int32 + binary.BigEndian.PutUint32(res, value) + return copy(target, res) +} +func PickleUInt32Len(value uint32) int { + return 4 +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go new file mode 100644 index 0000000..9a6a4b6 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go @@ -0,0 +1,53 @@ +package libolmpickle + +import ( + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" +) + +func isZeroByteSlice(bytes []byte) bool { + b := byte(0) + for _, s := range bytes { + b |= s + } + return b == 0 +} + +func UnpickleUInt8(value []byte) (uint8, int, error) { + if len(value) < 1 { + return 0, 0, fmt.Errorf("unpickle uint8: %w", goolm.ErrValueTooShort) + } + return value[0], 1, nil +} + +func UnpickleBool(value []byte) (bool, int, error) { + if len(value) < 1 { + return false, 0, fmt.Errorf("unpickle bool: %w", goolm.ErrValueTooShort) + } + return value[0] != uint8(0x00), 1, nil +} + +func UnpickleBytes(value []byte, length int) ([]byte, int, error) { + if len(value) < length { + return nil, 0, fmt.Errorf("unpickle bytes: %w", goolm.ErrValueTooShort) + } + resp := value[:length] + if isZeroByteSlice(resp) { + return nil, length, nil + } + return resp, length, nil +} + +func UnpickleUInt32(value []byte) (uint32, int, error) { + if len(value) < 4 { + return 0, 0, fmt.Errorf("unpickle uint32: %w", goolm.ErrValueTooShort) + } + var res uint32 + count := 0 + for i := 3; i >= 0; i-- { + res |= uint32(value[count]) << (8 * i) + count++ + } + return res, 4, nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/main.go b/vendor/maunium.net/go/mautrix/crypto/goolm/main.go new file mode 100644 index 0000000..5567430 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/main.go @@ -0,0 +1,6 @@ +// Package goolm is a pure Go implementation of libolm. Libolm is a cryptographic library used for end-to-end encryption in Matrix and written in C++. +// With goolm there is no need to use cgo when building Matrix clients in go. +/* +This package contains the possible errors which can occur as well as some simple functions. All the 'action' happens in the subdirectories. +*/ +package goolm diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/megolm/megolm.go b/vendor/maunium.net/go/mautrix/crypto/goolm/megolm/megolm.go new file mode 100644 index 0000000..c3493f7 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/megolm/megolm.go @@ -0,0 +1,234 @@ +// megolm provides the ratchet used by the megolm protocol +package megolm + +import ( + "crypto/rand" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/goolm/message" + "maunium.net/go/mautrix/crypto/goolm/utilities" +) + +const ( + megolmPickleVersion uint8 = 1 +) + +const ( + protocolVersion = 3 + RatchetParts = 4 // number of ratchet parts + RatchetPartLength = 256 / 8 // length of each ratchet part in bytes +) + +var RatchetCipher = cipher.NewAESSHA256([]byte("MEGOLM_KEYS")) + +// hasKeySeed are the seed for the different ratchet parts +var hashKeySeeds [RatchetParts][]byte = [RatchetParts][]byte{ + {0x00}, + {0x01}, + {0x02}, + {0x03}, +} + +// Ratchet represents the megolm ratchet as described in +// +// https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/megolm.md +type Ratchet struct { + Data [RatchetParts * RatchetPartLength]byte `json:"data"` + Counter uint32 `json:"counter"` +} + +// New creates a new ratchet with counter set to counter and the ratchet data set to data. +func New(counter uint32, data [RatchetParts * RatchetPartLength]byte) (*Ratchet, error) { + m := &Ratchet{ + Counter: counter, + Data: data, + } + return m, nil +} + +// NewWithRandom creates a new ratchet with counter set to counter an the data filled with random values. +func NewWithRandom(counter uint32) (*Ratchet, error) { + var data [RatchetParts * RatchetPartLength]byte + _, err := rand.Read(data[:]) + if err != nil { + return nil, err + } + return New(counter, data) +} + +// rehashPart rehases the part of the ratchet data with the base defined as from storing into the target to. +func (m *Ratchet) rehashPart(from, to int) { + newData := crypto.HMACSHA256(m.Data[from*RatchetPartLength:from*RatchetPartLength+RatchetPartLength], hashKeySeeds[to]) + copy(m.Data[to*RatchetPartLength:], newData[:RatchetPartLength]) +} + +// Advance advances the ratchet one step. +func (m *Ratchet) Advance() { + var mask uint32 = 0x00FFFFFF + var h int + m.Counter++ + + // figure out how much we need to rekey + for h < RatchetParts { + if (m.Counter & mask) == 0 { + break + } + h++ + mask >>= 8 + } + + // now update R(h)...R(3) based on R(h) + for i := RatchetParts - 1; i >= h; i-- { + m.rehashPart(h, i) + } +} + +// AdvanceTo advances the ratchet so that the ratchet counter = target +func (m *Ratchet) AdvanceTo(target uint32) { + //starting with R0, see if we need to update each part of the hash + for j := 0; j < RatchetParts; j++ { + shift := uint32((RatchetParts - j - 1) * 8) + mask := (^uint32(0)) << shift + + // how many times do we need to rehash this part? + // '& 0xff' ensures we handle integer wraparound correctly + steps := ((target >> shift) - m.Counter>>shift) & uint32(0xff) + + if steps == 0 { + /* + deal with the edge case where m.Counter is slightly larger + than target. This should only happen for R(0), and implies + that target has wrapped around and we need to advance R(0) + 256 times. + */ + if target < m.Counter { + steps = 0x100 + } else { + continue + } + } + // for all but the last step, we can just bump R(j) without regard to R(j+1)...R(3). + for steps > 1 { + m.rehashPart(j, j) + steps-- + } + /* + on the last step we also need to bump R(j+1)...R(3). + + (Theoretically, we could skip bumping R(j+2) if we're going to bump + R(j+1) again, but the code to figure that out is a bit baroque and + doesn't save us much). + */ + for k := 3; k >= j; k-- { + m.rehashPart(j, k) + } + m.Counter = target & mask + } +} + +// Encrypt encrypts the message in a message.GroupMessage with MAC and signature. +// The output is base64 encoded. +func (r *Ratchet) Encrypt(plaintext []byte, key *crypto.Ed25519KeyPair) ([]byte, error) { + var err error + encryptedText, err := RatchetCipher.Encrypt(r.Data[:], plaintext) + if err != nil { + return nil, fmt.Errorf("cipher encrypt: %w", err) + } + + message := &message.GroupMessage{} + message.Version = protocolVersion + message.MessageIndex = r.Counter + message.Ciphertext = encryptedText + //creating the mac and signing is done in encode + output, err := message.EncodeAndMacAndSign(r.Data[:], RatchetCipher, key) + if err != nil { + return nil, err + } + r.Advance() + return output, nil +} + +// SessionSharingMessage creates a message in the session sharing format. +func (r Ratchet) SessionSharingMessage(key crypto.Ed25519KeyPair) ([]byte, error) { + m := message.MegolmSessionSharing{} + m.Counter = r.Counter + m.RatchetData = r.Data + encoded := m.EncodeAndSign(key) + return goolm.Base64Encode(encoded), nil +} + +// SessionExportMessage creates a message in the session export format. +func (r Ratchet) SessionExportMessage(key crypto.Ed25519PublicKey) ([]byte, error) { + m := message.MegolmSessionExport{} + m.Counter = r.Counter + m.RatchetData = r.Data + m.PublicKey = key + encoded := m.Encode() + return goolm.Base64Encode(encoded), nil +} + +// Decrypt decrypts the ciphertext and verifies the MAC but not the signature. +func (r Ratchet) Decrypt(ciphertext []byte, signingkey *crypto.Ed25519PublicKey, msg *message.GroupMessage) ([]byte, error) { + //verify mac + verifiedMAC, err := msg.VerifyMACInline(r.Data[:], RatchetCipher, ciphertext) + if err != nil { + return nil, err + } + if !verifiedMAC { + return nil, fmt.Errorf("decrypt: %w", goolm.ErrBadMAC) + } + + return RatchetCipher.Decrypt(r.Data[:], msg.Ciphertext) +} + +// PickleAsJSON returns a ratchet as a base64 string encrypted using the supplied key. The unencrypted representation of the Account is in JSON format. +func (r Ratchet) PickleAsJSON(key []byte) ([]byte, error) { + return utilities.PickleAsJSON(r, megolmPickleVersion, key) +} + +// UnpickleAsJSON updates a ratchet by a base64 encrypted string using the supplied key. The unencrypted representation has to be in JSON format. +func (r *Ratchet) UnpickleAsJSON(pickled, key []byte) error { + return utilities.UnpickleAsJSON(r, pickled, key, megolmPickleVersion) +} + +// UnpickleLibOlm decodes the unencryted value and populates the Ratchet accordingly. It returns the number of bytes read. +func (r *Ratchet) UnpickleLibOlm(unpickled []byte) (int, error) { + //read ratchet data + curPos := 0 + ratchetData, readBytes, err := libolmpickle.UnpickleBytes(unpickled, RatchetParts*RatchetPartLength) + if err != nil { + return 0, err + } + copy(r.Data[:], ratchetData) + curPos += readBytes + //Read counter + counter, readBytes, err := libolmpickle.UnpickleUInt32(unpickled[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + r.Counter = counter + return curPos, nil +} + +// PickleLibOlm encodes the ratchet into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (r Ratchet) PickleLibOlm(target []byte) (int, error) { + if len(target) < r.PickleLen() { + return 0, fmt.Errorf("pickle account: %w", goolm.ErrValueTooShort) + } + written := libolmpickle.PickleBytes(r.Data[:], target) + written += libolmpickle.PickleUInt32(r.Counter, target[written:]) + return written, nil +} + +// PickleLen returns the number of bytes the pickled ratchet will have. +func (r Ratchet) PickleLen() int { + length := libolmpickle.PickleBytesLen(r.Data[:]) + length += libolmpickle.PickleUInt32Len(r.Counter) + return length +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/message/decoder.go b/vendor/maunium.net/go/mautrix/crypto/goolm/message/decoder.go new file mode 100644 index 0000000..ba49f01 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/message/decoder.go @@ -0,0 +1,70 @@ +package message + +import ( + "encoding/binary" + + "maunium.net/go/mautrix/crypto/goolm" +) + +// checkDecodeErr checks if there was an error during decode. +func checkDecodeErr(readBytes int) error { + if readBytes == 0 { + //end reached + return goolm.ErrInputToSmall + } + if readBytes < 0 { + return goolm.ErrOverflow + } + return nil +} + +// decodeVarInt decodes a single big-endian encoded varint. +func decodeVarInt(input []byte) (uint32, int) { + value, readBytes := binary.Uvarint(input) + return uint32(value), readBytes +} + +// decodeVarString decodes the length of the string (varint) and returns the actual string +func decodeVarString(input []byte) ([]byte, int) { + stringLen, readBytes := decodeVarInt(input) + if readBytes <= 0 { + return nil, readBytes + } + input = input[readBytes:] + value := input[:stringLen] + readBytes += int(stringLen) + return value, readBytes +} + +// encodeVarIntByteLength returns the number of bytes needed to encode the uint32. +func encodeVarIntByteLength(input uint32) int { + result := 1 + for input >= 128 { + result++ + input >>= 7 + } + return result +} + +// encodeVarStringByteLength returns the number of bytes needed to encode the input. +func encodeVarStringByteLength(input []byte) int { + result := encodeVarIntByteLength(uint32(len(input))) + result += len(input) + return result +} + +// encodeVarInt encodes a single uint32 +func encodeVarInt(input uint32) []byte { + out := make([]byte, encodeVarIntByteLength(input)) + binary.PutUvarint(out, uint64(input)) + return out +} + +// encodeVarString encodes the length of the input (varint) and appends the actual input +func encodeVarString(input []byte) []byte { + out := make([]byte, encodeVarStringByteLength(input)) + length := encodeVarInt(uint32(len(input))) + copy(out, length) + copy(out[len(length):], input) + return out +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/message/group_message.go b/vendor/maunium.net/go/mautrix/crypto/goolm/message/group_message.go new file mode 100644 index 0000000..ebd5b77 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/message/group_message.go @@ -0,0 +1,144 @@ +package message + +import ( + "bytes" + + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +const ( + messageIndexTag = 0x08 + cipherTextTag = 0x12 + countMACBytesGroupMessage = 8 +) + +// GroupMessage represents a message in the group message format. +type GroupMessage struct { + Version byte `json:"version"` + MessageIndex uint32 `json:"index"` + Ciphertext []byte `json:"ciphertext"` + HasMessageIndex bool `json:"has_index"` +} + +// Decodes decodes the input and populates the corresponding fileds. MAC and signature are ignored but have to be present. +func (r *GroupMessage) Decode(input []byte) error { + r.Version = 0 + r.MessageIndex = 0 + r.Ciphertext = nil + if len(input) == 0 { + return nil + } + //first Byte is always version + r.Version = input[0] + curPos := 1 + for curPos < len(input)-countMACBytesGroupMessage-crypto.ED25519SignatureSize { + //Read Key + curKey, readBytes := decodeVarInt(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + if (curKey & 0b111) == 0 { + //The value is of type varint + value, readBytes := decodeVarInt(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + switch curKey { + case messageIndexTag: + r.MessageIndex = value + r.HasMessageIndex = true + } + } else if (curKey & 0b111) == 2 { + //The value is of type string + value, readBytes := decodeVarString(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + switch curKey { + case cipherTextTag: + r.Ciphertext = value + } + } + } + + return nil +} + +// EncodeAndMacAndSign encodes the message, creates the mac with the key and the cipher and signs the message. +// If macKey or cipher is nil, no mac is appended. If signKey is nil, no signature is appended. +func (r *GroupMessage) EncodeAndMacAndSign(macKey []byte, cipher cipher.Cipher, signKey *crypto.Ed25519KeyPair) ([]byte, error) { + var lengthOfMessage int + lengthOfMessage += 1 //Version + lengthOfMessage += encodeVarIntByteLength(messageIndexTag) + encodeVarIntByteLength(r.MessageIndex) + lengthOfMessage += encodeVarIntByteLength(cipherTextTag) + encodeVarStringByteLength(r.Ciphertext) + out := make([]byte, lengthOfMessage) + out[0] = r.Version + curPos := 1 + encodedTag := encodeVarInt(messageIndexTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue := encodeVarInt(r.MessageIndex) + copy(out[curPos:], encodedValue) + curPos += len(encodedValue) + encodedTag = encodeVarInt(cipherTextTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue = encodeVarString(r.Ciphertext) + copy(out[curPos:], encodedValue) + curPos += len(encodedValue) + if len(macKey) != 0 && cipher != nil { + mac, err := r.MAC(macKey, cipher, out) + if err != nil { + return nil, err + } + out = append(out, mac[:countMACBytesGroupMessage]...) + } + if signKey != nil { + signature := signKey.Sign(out) + out = append(out, signature...) + } + return out, nil +} + +// MAC returns the MAC of the message calculated with cipher and key. The length of the MAC is truncated to the correct length. +func (r *GroupMessage) MAC(key []byte, cipher cipher.Cipher, message []byte) ([]byte, error) { + mac, err := cipher.MAC(key, message) + if err != nil { + return nil, err + } + return mac[:countMACBytesGroupMessage], nil +} + +// VerifySignature verifies the givenSignature to the calculated signature of the message. +func (r *GroupMessage) VerifySignature(key crypto.Ed25519PublicKey, message, givenSignature []byte) bool { + return key.Verify(message, givenSignature) +} + +// VerifySignature verifies the signature taken from the message to the calculated signature of the message. +func (r *GroupMessage) VerifySignatureInline(key crypto.Ed25519PublicKey, message []byte) bool { + signature := message[len(message)-crypto.ED25519SignatureSize:] + message = message[:len(message)-crypto.ED25519SignatureSize] + return key.Verify(message, signature) +} + +// VerifyMAC verifies the givenMAC to the calculated MAC of the message. +func (r *GroupMessage) VerifyMAC(key []byte, cipher cipher.Cipher, message, givenMAC []byte) (bool, error) { + checkMac, err := r.MAC(key, cipher, message) + if err != nil { + return false, err + } + return bytes.Equal(checkMac[:countMACBytesGroupMessage], givenMAC), nil +} + +// VerifyMACInline verifies the MAC taken from the message to the calculated MAC of the message. +func (r *GroupMessage) VerifyMACInline(key []byte, cipher cipher.Cipher, message []byte) (bool, error) { + startMAC := len(message) - countMACBytesGroupMessage - crypto.ED25519SignatureSize + endMAC := startMAC + countMACBytesGroupMessage + suplMac := message[startMAC:endMAC] + message = message[:startMAC] + return r.VerifyMAC(key, cipher, message, suplMac) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/message/message.go b/vendor/maunium.net/go/mautrix/crypto/goolm/message/message.go new file mode 100644 index 0000000..8b721ae --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/message/message.go @@ -0,0 +1,129 @@ +package message + +import ( + "bytes" + + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +const ( + ratchetKeyTag = 0x0A + counterTag = 0x10 + cipherTextKeyTag = 0x22 + countMACBytesMessage = 8 +) + +// GroupMessage represents a message in the message format. +type Message struct { + Version byte `json:"version"` + HasCounter bool `json:"has_counter"` + Counter uint32 `json:"counter"` + RatchetKey crypto.Curve25519PublicKey `json:"ratchet_key"` + Ciphertext []byte `json:"ciphertext"` +} + +// Decodes decodes the input and populates the corresponding fileds. MAC is ignored but has to be present. +func (r *Message) Decode(input []byte) error { + r.Version = 0 + r.HasCounter = false + r.Counter = 0 + r.RatchetKey = nil + r.Ciphertext = nil + if len(input) == 0 { + return nil + } + //first Byte is always version + r.Version = input[0] + curPos := 1 + for curPos < len(input)-countMACBytesMessage { + //Read Key + curKey, readBytes := decodeVarInt(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + if (curKey & 0b111) == 0 { + //The value is of type varint + value, readBytes := decodeVarInt(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + switch curKey { + case counterTag: + r.HasCounter = true + r.Counter = value + } + } else if (curKey & 0b111) == 2 { + //The value is of type string + value, readBytes := decodeVarString(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + switch curKey { + case ratchetKeyTag: + r.RatchetKey = value + case cipherTextKeyTag: + r.Ciphertext = value + } + } + } + + return nil +} + +// EncodeAndMAC encodes the message and creates the MAC with the key and the cipher. +// If key or cipher is nil, no MAC is appended. +func (r *Message) EncodeAndMAC(key []byte, cipher cipher.Cipher) ([]byte, error) { + var lengthOfMessage int + lengthOfMessage += 1 //Version + lengthOfMessage += encodeVarIntByteLength(ratchetKeyTag) + encodeVarStringByteLength(r.RatchetKey) + lengthOfMessage += encodeVarIntByteLength(counterTag) + encodeVarIntByteLength(r.Counter) + lengthOfMessage += encodeVarIntByteLength(cipherTextKeyTag) + encodeVarStringByteLength(r.Ciphertext) + out := make([]byte, lengthOfMessage) + out[0] = r.Version + curPos := 1 + encodedTag := encodeVarInt(ratchetKeyTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue := encodeVarString(r.RatchetKey) + copy(out[curPos:], encodedValue) + curPos += len(encodedValue) + encodedTag = encodeVarInt(counterTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue = encodeVarInt(r.Counter) + copy(out[curPos:], encodedValue) + curPos += len(encodedValue) + encodedTag = encodeVarInt(cipherTextKeyTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue = encodeVarString(r.Ciphertext) + copy(out[curPos:], encodedValue) + curPos += len(encodedValue) + if len(key) != 0 && cipher != nil { + mac, err := cipher.MAC(key, out) + if err != nil { + return nil, err + } + out = append(out, mac[:countMACBytesMessage]...) + } + return out, nil +} + +// VerifyMAC verifies the givenMAC to the calculated MAC of the message. +func (r *Message) VerifyMAC(key []byte, cipher cipher.Cipher, message, givenMAC []byte) (bool, error) { + checkMAC, err := cipher.MAC(key, message) + if err != nil { + return false, err + } + return bytes.Equal(checkMAC[:countMACBytesMessage], givenMAC), nil +} + +// VerifyMACInline verifies the MAC taken from the message to the calculated MAC of the message. +func (r *Message) VerifyMACInline(key []byte, cipher cipher.Cipher, message []byte) (bool, error) { + givenMAC := message[len(message)-countMACBytesMessage:] + return r.VerifyMAC(key, cipher, message[:len(message)-countMACBytesMessage], givenMAC) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/message/prekey_message.go b/vendor/maunium.net/go/mautrix/crypto/goolm/message/prekey_message.go new file mode 100644 index 0000000..6e007e0 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/message/prekey_message.go @@ -0,0 +1,120 @@ +package message + +import ( + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +const ( + oneTimeKeyIdTag = 0x0A + baseKeyTag = 0x12 + identityKeyTag = 0x1A + messageTag = 0x22 +) + +type PreKeyMessage struct { + Version byte `json:"version"` + IdentityKey crypto.Curve25519PublicKey `json:"id_key"` + BaseKey crypto.Curve25519PublicKey `json:"base_key"` + OneTimeKey crypto.Curve25519PublicKey `json:"one_time_key"` + Message []byte `json:"message"` +} + +// Decodes decodes the input and populates the corresponding fileds. +func (r *PreKeyMessage) Decode(input []byte) error { + r.Version = 0 + r.IdentityKey = nil + r.BaseKey = nil + r.OneTimeKey = nil + r.Message = nil + if len(input) == 0 { + return nil + } + //first Byte is always version + r.Version = input[0] + curPos := 1 + for curPos < len(input) { + //Read Key + curKey, readBytes := decodeVarInt(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + if (curKey & 0b111) == 0 { + //The value is of type varint + _, readBytes := decodeVarInt(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + } else if (curKey & 0b111) == 2 { + //The value is of type string + value, readBytes := decodeVarString(input[curPos:]) + if err := checkDecodeErr(readBytes); err != nil { + return err + } + curPos += readBytes + switch curKey { + case oneTimeKeyIdTag: + r.OneTimeKey = value + case baseKeyTag: + r.BaseKey = value + case identityKeyTag: + r.IdentityKey = value + case messageTag: + r.Message = value + } + } + } + + return nil +} + +// CheckField verifies the fields. If theirIdentityKey is nil, it is not compared to the key in the message. +func (r *PreKeyMessage) CheckFields(theirIdentityKey *crypto.Curve25519PublicKey) bool { + ok := true + ok = ok && (theirIdentityKey != nil || r.IdentityKey != nil) + if r.IdentityKey != nil { + ok = ok && (len(r.IdentityKey) == crypto.Curve25519KeyLength) + } + ok = ok && len(r.Message) != 0 + ok = ok && len(r.BaseKey) == crypto.Curve25519KeyLength + ok = ok && len(r.OneTimeKey) == crypto.Curve25519KeyLength + return ok +} + +// Encode encodes the message. +func (r *PreKeyMessage) Encode() ([]byte, error) { + var lengthOfMessage int + lengthOfMessage += 1 //Version + lengthOfMessage += encodeVarIntByteLength(oneTimeKeyIdTag) + encodeVarStringByteLength(r.OneTimeKey) + lengthOfMessage += encodeVarIntByteLength(identityKeyTag) + encodeVarStringByteLength(r.IdentityKey) + lengthOfMessage += encodeVarIntByteLength(baseKeyTag) + encodeVarStringByteLength(r.BaseKey) + lengthOfMessage += encodeVarIntByteLength(messageTag) + encodeVarStringByteLength(r.Message) + out := make([]byte, lengthOfMessage) + out[0] = r.Version + curPos := 1 + encodedTag := encodeVarInt(oneTimeKeyIdTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue := encodeVarString(r.OneTimeKey) + copy(out[curPos:], encodedValue) + curPos += len(encodedValue) + encodedTag = encodeVarInt(identityKeyTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue = encodeVarString(r.IdentityKey) + copy(out[curPos:], encodedValue) + curPos += len(encodedValue) + encodedTag = encodeVarInt(baseKeyTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue = encodeVarString(r.BaseKey) + copy(out[curPos:], encodedValue) + curPos += len(encodedValue) + encodedTag = encodeVarInt(messageTag) + copy(out[curPos:], encodedTag) + curPos += len(encodedTag) + encodedValue = encodeVarString(r.Message) + copy(out[curPos:], encodedValue) + return out, nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/message/session_export.go b/vendor/maunium.net/go/mautrix/crypto/goolm/message/session_export.go new file mode 100644 index 0000000..f539cce --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/message/session_export.go @@ -0,0 +1,44 @@ +package message + +import ( + "encoding/binary" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +const ( + sessionExportVersion = 0x01 +) + +// MegolmSessionExport represents a message in the session export format. +type MegolmSessionExport struct { + Counter uint32 `json:"counter"` + RatchetData [128]byte `json:"data"` + PublicKey crypto.Ed25519PublicKey `json:"public_key"` +} + +// Encode returns the encoded message in the correct format. +func (s MegolmSessionExport) Encode() []byte { + output := make([]byte, 165) + output[0] = sessionExportVersion + binary.BigEndian.PutUint32(output[1:], s.Counter) + copy(output[5:], s.RatchetData[:]) + copy(output[133:], s.PublicKey) + return output +} + +// Decode populates the struct with the data encoded in input. +func (s *MegolmSessionExport) Decode(input []byte) error { + if len(input) != 165 { + return fmt.Errorf("decrypt: %w", goolm.ErrBadInput) + } + if input[0] != sessionExportVersion { + return fmt.Errorf("decrypt: %w", goolm.ErrBadVersion) + } + s.Counter = binary.BigEndian.Uint32(input[1:5]) + copy(s.RatchetData[:], input[5:133]) + s.PublicKey = input[133:] + return nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/message/session_sharing.go b/vendor/maunium.net/go/mautrix/crypto/goolm/message/session_sharing.go new file mode 100644 index 0000000..c5393f5 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/message/session_sharing.go @@ -0,0 +1,50 @@ +package message + +import ( + "encoding/binary" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +const ( + sessionSharingVersion = 0x02 +) + +// MegolmSessionSharing represents a message in the session sharing format. +type MegolmSessionSharing struct { + Counter uint32 `json:"counter"` + RatchetData [128]byte `json:"data"` + PublicKey crypto.Ed25519PublicKey `json:"-"` //only used when decrypting messages +} + +// Encode returns the encoded message in the correct format with the signature by key appended. +func (s MegolmSessionSharing) EncodeAndSign(key crypto.Ed25519KeyPair) []byte { + output := make([]byte, 229) + output[0] = sessionSharingVersion + binary.BigEndian.PutUint32(output[1:], s.Counter) + copy(output[5:], s.RatchetData[:]) + copy(output[133:], key.PublicKey) + signature := key.Sign(output[:165]) + copy(output[165:], signature) + return output +} + +// VerifyAndDecode verifies the input and populates the struct with the data encoded in input. +func (s *MegolmSessionSharing) VerifyAndDecode(input []byte) error { + if len(input) != 229 { + return fmt.Errorf("verify: %w", goolm.ErrBadInput) + } + publicKey := crypto.Ed25519PublicKey(input[133:165]) + if !publicKey.Verify(input[:165], input[165:]) { + return fmt.Errorf("verify: %w", goolm.ErrBadVerification) + } + s.PublicKey = publicKey + if input[0] != sessionSharingVersion { + return fmt.Errorf("verify: %w", goolm.ErrBadVersion) + } + s.Counter = binary.BigEndian.Uint32(input[1:5]) + copy(s.RatchetData[:], input[5:133]) + return nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/olm/chain.go b/vendor/maunium.net/go/mautrix/crypto/goolm/olm/chain.go new file mode 100644 index 0000000..403637a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/olm/chain.go @@ -0,0 +1,258 @@ +package olm + +import ( + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" +) + +const ( + chainKeySeed = 0x02 + messageKeyLength = 32 +) + +// chainKey wraps the index and the public key +type chainKey struct { + Index uint32 `json:"index"` + Key crypto.Curve25519PublicKey `json:"key"` +} + +// advance advances the chain +func (c *chainKey) advance() { + c.Key = crypto.HMACSHA256(c.Key, []byte{chainKeySeed}) + c.Index++ +} + +// UnpickleLibOlm decodes the unencryted value and populates the chain key accordingly. It returns the number of bytes read. +func (r *chainKey) UnpickleLibOlm(value []byte) (int, error) { + curPos := 0 + readBytes, err := r.Key.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + curPos += readBytes + r.Index, readBytes, err = libolmpickle.UnpickleUInt32(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + return curPos, nil +} + +// PickleLibOlm encodes the chain key into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (r chainKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < r.PickleLen() { + return 0, fmt.Errorf("pickle chain key: %w", goolm.ErrValueTooShort) + } + written, err := r.Key.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle chain key: %w", err) + } + written += libolmpickle.PickleUInt32(r.Index, target[written:]) + return written, nil +} + +// PickleLen returns the number of bytes the pickled chain key will have. +func (r chainKey) PickleLen() int { + length := r.Key.PickleLen() + length += libolmpickle.PickleUInt32Len(r.Index) + return length +} + +// senderChain is a chain for sending messages +type senderChain struct { + RKey crypto.Curve25519KeyPair `json:"ratchet_key"` + CKey chainKey `json:"chain_key"` + IsSet bool `json:"set"` +} + +// newSenderChain returns a sender chain initialized with chainKey and ratchet key pair. +func newSenderChain(key crypto.Curve25519PublicKey, ratchet crypto.Curve25519KeyPair) *senderChain { + return &senderChain{ + RKey: ratchet, + CKey: chainKey{ + Index: 0, + Key: key, + }, + IsSet: true, + } +} + +// advance advances the chain +func (s *senderChain) advance() { + s.CKey.advance() +} + +// ratchetKey returns the ratchet key pair. +func (s senderChain) ratchetKey() crypto.Curve25519KeyPair { + return s.RKey +} + +// chainKey returns the current chainKey. +func (s senderChain) chainKey() chainKey { + return s.CKey +} + +// UnpickleLibOlm decodes the unencryted value and populates the chain accordingly. It returns the number of bytes read. +func (r *senderChain) UnpickleLibOlm(value []byte) (int, error) { + curPos := 0 + readBytes, err := r.RKey.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = r.CKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + return curPos, nil +} + +// PickleLibOlm encodes the chain into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (r senderChain) PickleLibOlm(target []byte) (int, error) { + if len(target) < r.PickleLen() { + return 0, fmt.Errorf("pickle sender chain: %w", goolm.ErrValueTooShort) + } + written, err := r.RKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle sender chain: %w", err) + } + writtenChain, err := r.CKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle sender chain: %w", err) + } + written += writtenChain + return written, nil +} + +// PickleLen returns the number of bytes the pickled chain will have. +func (r senderChain) PickleLen() int { + length := r.RKey.PickleLen() + length += r.CKey.PickleLen() + return length +} + +// senderChain is a chain for receiving messages +type receiverChain struct { + RKey crypto.Curve25519PublicKey `json:"ratchet_key"` + CKey chainKey `json:"chain_key"` +} + +// newReceiverChain returns a receiver chain initialized with chainKey and ratchet public key. +func newReceiverChain(chain crypto.Curve25519PublicKey, ratchet crypto.Curve25519PublicKey) *receiverChain { + return &receiverChain{ + RKey: ratchet, + CKey: chainKey{ + Index: 0, + Key: chain, + }, + } +} + +// advance advances the chain +func (s *receiverChain) advance() { + s.CKey.advance() +} + +// ratchetKey returns the ratchet public key. +func (s receiverChain) ratchetKey() crypto.Curve25519PublicKey { + return s.RKey +} + +// chainKey returns the current chainKey. +func (s receiverChain) chainKey() chainKey { + return s.CKey +} + +// UnpickleLibOlm decodes the unencryted value and populates the chain accordingly. It returns the number of bytes read. +func (r *receiverChain) UnpickleLibOlm(value []byte) (int, error) { + curPos := 0 + readBytes, err := r.RKey.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = r.CKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + return curPos, nil +} + +// PickleLibOlm encodes the chain into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (r receiverChain) PickleLibOlm(target []byte) (int, error) { + if len(target) < r.PickleLen() { + return 0, fmt.Errorf("pickle sender chain: %w", goolm.ErrValueTooShort) + } + written, err := r.RKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle sender chain: %w", err) + } + writtenChain, err := r.CKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle sender chain: %w", err) + } + written += writtenChain + return written, nil +} + +// PickleLen returns the number of bytes the pickled chain will have. +func (r receiverChain) PickleLen() int { + length := r.RKey.PickleLen() + length += r.CKey.PickleLen() + return length +} + +// messageKey wraps the index and the key of a message +type messageKey struct { + Index uint32 `json:"index"` + Key []byte `json:"key"` +} + +// UnpickleLibOlm decodes the unencryted value and populates the message key accordingly. It returns the number of bytes read. +func (m *messageKey) UnpickleLibOlm(value []byte) (int, error) { + curPos := 0 + ratchetKey, readBytes, err := libolmpickle.UnpickleBytes(value, messageKeyLength) + if err != nil { + return 0, err + } + m.Key = ratchetKey + curPos += readBytes + keyID, readBytes, err := libolmpickle.UnpickleUInt32(value[:curPos]) + if err != nil { + return 0, err + } + curPos += readBytes + m.Index = keyID + return curPos, nil +} + +// PickleLibOlm encodes the message key into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (m messageKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < m.PickleLen() { + return 0, fmt.Errorf("pickle message key: %w", goolm.ErrValueTooShort) + } + written := 0 + if len(m.Key) != messageKeyLength { + written += libolmpickle.PickleBytes(make([]byte, messageKeyLength), target) + } else { + written += libolmpickle.PickleBytes(m.Key, target) + } + written += libolmpickle.PickleUInt32(m.Index, target[written:]) + return written, nil +} + +// PickleLen returns the number of bytes the pickled message key will have. +func (r messageKey) PickleLen() int { + length := libolmpickle.PickleBytesLen(make([]byte, messageKeyLength)) + length += libolmpickle.PickleUInt32Len(r.Index) + return length +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/olm/olm.go b/vendor/maunium.net/go/mautrix/crypto/goolm/olm/olm.go new file mode 100644 index 0000000..299ec7c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/olm/olm.go @@ -0,0 +1,432 @@ +// olm provides the ratchet used by the olm protocol +package olm + +import ( + "fmt" + "io" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/goolm/message" + "maunium.net/go/mautrix/crypto/goolm/utilities" +) + +const ( + olmPickleVersion uint8 = 1 +) + +const ( + maxReceiverChains = 5 + maxSkippedMessageKeys = 40 + protocolVersion = 3 + messageKeySeed = 0x01 + + maxMessageGap = 2000 + sharedKeyLength = 32 +) + +// KdfInfo has the infos used for the kdf +var KdfInfo = struct { + Root []byte + Ratchet []byte +}{ + Root: []byte("OLM_ROOT"), + Ratchet: []byte("OLM_RATCHET"), +} + +var RatchetCipher = cipher.NewAESSHA256([]byte("OLM_KEYS")) + +// Ratchet represents the olm ratchet as described in +// +// https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md +type Ratchet struct { + // The root key is used to generate chain keys from the ephemeral keys. + // A new root_key is derived each time a new chain is started. + RootKey crypto.Curve25519PublicKey `json:"root_key"` + + // The sender chain is used to send messages. Each time a new ephemeral + // key is received from the remote server we generate a new sender chain + // with a new ephemeral key when we next send a message. + SenderChains senderChain `json:"sender_chain"` + + // The receiver chain is used to decrypt received messages. We store the + // last few chains so we can decrypt any out of order messages we haven't + // received yet. + // New chains are prepended for easier access. + ReceiverChains []receiverChain `json:"receiver_chains"` + + // Storing the keys of missed messages for future use. + // The order of the elements is not important. + SkippedMessageKeys []skippedMessageKey `json:"skipped_message_keys"` +} + +// New creates a new ratchet, setting the kdfInfos and cipher. +func New() *Ratchet { + r := &Ratchet{} + return r +} + +// InitializeAsBob initializes this ratchet from a receiving point of view (only first message). +func (r *Ratchet) InitializeAsBob(sharedSecret []byte, theirRatchetKey crypto.Curve25519PublicKey) error { + derivedSecretsReader := crypto.HKDFSHA256(sharedSecret, nil, KdfInfo.Root) + derivedSecrets := make([]byte, 2*sharedKeyLength) + if _, err := io.ReadFull(derivedSecretsReader, derivedSecrets); err != nil { + return err + } + r.RootKey = derivedSecrets[0:sharedKeyLength] + newReceiverChain := newReceiverChain(derivedSecrets[sharedKeyLength:], theirRatchetKey) + r.ReceiverChains = append([]receiverChain{*newReceiverChain}, r.ReceiverChains...) + return nil +} + +// InitializeAsAlice initializes this ratchet from a sending point of view (only first message). +func (r *Ratchet) InitializeAsAlice(sharedSecret []byte, ourRatchetKey crypto.Curve25519KeyPair) error { + derivedSecretsReader := crypto.HKDFSHA256(sharedSecret, nil, KdfInfo.Root) + derivedSecrets := make([]byte, 2*sharedKeyLength) + if _, err := io.ReadFull(derivedSecretsReader, derivedSecrets); err != nil { + return err + } + r.RootKey = derivedSecrets[0:sharedKeyLength] + newSenderChain := newSenderChain(derivedSecrets[sharedKeyLength:], ourRatchetKey) + r.SenderChains = *newSenderChain + return nil +} + +// Encrypt encrypts the message in a message.Message with MAC. If reader is nil, crypto/rand is used for key generations. +func (r *Ratchet) Encrypt(plaintext []byte, reader io.Reader) ([]byte, error) { + var err error + if !r.SenderChains.IsSet { + newRatchetKey, err := crypto.Curve25519GenerateKey(reader) + if err != nil { + return nil, err + } + newChainKey, err := r.advanceRootKey(newRatchetKey, r.ReceiverChains[0].ratchetKey()) + if err != nil { + return nil, err + } + newSenderChain := newSenderChain(newChainKey, newRatchetKey) + r.SenderChains = *newSenderChain + } + + messageKey := r.createMessageKeys(r.SenderChains.chainKey()) + r.SenderChains.advance() + + encryptedText, err := RatchetCipher.Encrypt(messageKey.Key, plaintext) + if err != nil { + return nil, fmt.Errorf("cipher encrypt: %w", err) + } + + message := &message.Message{} + message.Version = protocolVersion + message.Counter = messageKey.Index + message.RatchetKey = r.SenderChains.ratchetKey().PublicKey + message.Ciphertext = encryptedText + //creating the mac is done in encode + output, err := message.EncodeAndMAC(messageKey.Key, RatchetCipher) + if err != nil { + return nil, err + } + + return output, nil +} + +// Decrypt decrypts the ciphertext and verifies the MAC. If reader is nil, crypto/rand is used for key generations. +func (r *Ratchet) Decrypt(input []byte) ([]byte, error) { + message := &message.Message{} + //The mac is not verified here, as we do not know the key yet + err := message.Decode(input) + if err != nil { + return nil, err + } + if message.Version != protocolVersion { + return nil, fmt.Errorf("decrypt: %w", goolm.ErrWrongProtocolVersion) + } + if !message.HasCounter || len(message.RatchetKey) == 0 || len(message.Ciphertext) == 0 { + return nil, fmt.Errorf("decrypt: %w", goolm.ErrBadMessageFormat) + } + var receiverChainFromMessage *receiverChain + for curChainIndex := range r.ReceiverChains { + if r.ReceiverChains[curChainIndex].ratchetKey().Equal(message.RatchetKey) { + receiverChainFromMessage = &r.ReceiverChains[curChainIndex] + break + } + } + var result []byte + if receiverChainFromMessage == nil { + //Advancing the chain is done in this method + result, err = r.decryptForNewChain(message, input) + if err != nil { + return nil, err + } + } else if receiverChainFromMessage.chainKey().Index > message.Counter { + // No need to advance the chain + // Chain already advanced beyond the key for this message + // Check if the message keys are in the skipped key list. + foundSkippedKey := false + for curSkippedIndex := range r.SkippedMessageKeys { + if message.Counter == r.SkippedMessageKeys[curSkippedIndex].MKey.Index { + // Found the key for this message. Check the MAC. + verified, err := message.VerifyMACInline(r.SkippedMessageKeys[curSkippedIndex].MKey.Key, RatchetCipher, input) + if err != nil { + return nil, err + } + if !verified { + return nil, fmt.Errorf("decrypt from skipped message keys: %w", goolm.ErrBadMAC) + } + result, err = RatchetCipher.Decrypt(r.SkippedMessageKeys[curSkippedIndex].MKey.Key, message.Ciphertext) + if err != nil { + return nil, fmt.Errorf("cipher decrypt: %w", err) + } + if len(result) != 0 { + // Remove the key from the skipped keys now that we've + // decoded the message it corresponds to. + r.SkippedMessageKeys[curSkippedIndex] = r.SkippedMessageKeys[len(r.SkippedMessageKeys)-1] + r.SkippedMessageKeys = r.SkippedMessageKeys[:len(r.SkippedMessageKeys)-1] + } + foundSkippedKey = true + } + } + if !foundSkippedKey { + return nil, fmt.Errorf("decrypt: %w", goolm.ErrMessageKeyNotFound) + } + } else { + //Advancing the chain is done in this method + result, err = r.decryptForExistingChain(receiverChainFromMessage, message, input) + if err != nil { + return nil, err + } + } + + return result, nil +} + +// advanceRootKey created the next root key and returns the next chainKey +func (r *Ratchet) advanceRootKey(newRatchetKey crypto.Curve25519KeyPair, oldRatchetKey crypto.Curve25519PublicKey) (crypto.Curve25519PublicKey, error) { + sharedSecret, err := newRatchetKey.SharedSecret(oldRatchetKey) + if err != nil { + return nil, err + } + derivedSecretsReader := crypto.HKDFSHA256(sharedSecret, r.RootKey, KdfInfo.Ratchet) + derivedSecrets := make([]byte, 2*sharedKeyLength) + if _, err := io.ReadFull(derivedSecretsReader, derivedSecrets); err != nil { + return nil, err + } + r.RootKey = derivedSecrets[:sharedKeyLength] + return derivedSecrets[sharedKeyLength:], nil +} + +// createMessageKeys returns the messageKey derived from the chainKey +func (r Ratchet) createMessageKeys(chainKey chainKey) messageKey { + res := messageKey{} + res.Key = crypto.HMACSHA256(chainKey.Key, []byte{messageKeySeed}) + res.Index = chainKey.Index + return res +} + +// decryptForExistingChain returns the decrypted message by using the chain. The MAC of the rawMessage is verified. +func (r *Ratchet) decryptForExistingChain(chain *receiverChain, message *message.Message, rawMessage []byte) ([]byte, error) { + if message.Counter < chain.CKey.Index { + return nil, fmt.Errorf("decrypt: %w", goolm.ErrChainTooHigh) + } + // Limit the number of hashes we're prepared to compute + if message.Counter-chain.CKey.Index > maxMessageGap { + return nil, fmt.Errorf("decrypt from existing chain: %w", goolm.ErrMsgIndexTooHigh) + } + for chain.CKey.Index < message.Counter { + messageKey := r.createMessageKeys(chain.chainKey()) + skippedKey := skippedMessageKey{ + MKey: messageKey, + RKey: chain.ratchetKey(), + } + r.SkippedMessageKeys = append(r.SkippedMessageKeys, skippedKey) + chain.advance() + } + messageKey := r.createMessageKeys(chain.chainKey()) + chain.advance() + verified, err := message.VerifyMACInline(messageKey.Key, RatchetCipher, rawMessage) + if err != nil { + return nil, err + } + if !verified { + return nil, fmt.Errorf("decrypt from existing chain: %w", goolm.ErrBadMAC) + } + return RatchetCipher.Decrypt(messageKey.Key, message.Ciphertext) +} + +// decryptForNewChain returns the decrypted message by creating a new chain and advancing the root key. +func (r *Ratchet) decryptForNewChain(message *message.Message, rawMessage []byte) ([]byte, error) { + // They shouldn't move to a new chain until we've sent them a message + // acknowledging the last one + if !r.SenderChains.IsSet { + return nil, fmt.Errorf("decrypt for new chain: %w", goolm.ErrProtocolViolation) + } + // Limit the number of hashes we're prepared to compute + if message.Counter > maxMessageGap { + return nil, fmt.Errorf("decrypt for new chain: %w", goolm.ErrMsgIndexTooHigh) + } + + newChainKey, err := r.advanceRootKey(r.SenderChains.ratchetKey(), message.RatchetKey) + if err != nil { + return nil, err + } + newChain := newReceiverChain(newChainKey, message.RatchetKey) + r.ReceiverChains = append([]receiverChain{*newChain}, r.ReceiverChains...) + /* + They have started using a new ephemeral ratchet key. + We needed to derive a new set of chain keys. + We can discard our previous ephemeral ratchet key. + We will generate a new key when we send the next message. + */ + r.SenderChains = senderChain{} + + decrypted, err := r.decryptForExistingChain(&r.ReceiverChains[0], message, rawMessage) + if err != nil { + return nil, err + } + return decrypted, nil +} + +// PickleAsJSON returns a ratchet as a base64 string encrypted using the supplied key. The unencrypted representation of the Account is in JSON format. +func (r Ratchet) PickleAsJSON(key []byte) ([]byte, error) { + return utilities.PickleAsJSON(r, olmPickleVersion, key) +} + +// UnpickleAsJSON updates a ratchet by a base64 encrypted string using the supplied key. The unencrypted representation has to be in JSON format. +func (r *Ratchet) UnpickleAsJSON(pickled, key []byte) error { + return utilities.UnpickleAsJSON(r, pickled, key, olmPickleVersion) +} + +// UnpickleLibOlm decodes the unencryted value and populates the Ratchet accordingly. It returns the number of bytes read. +func (r *Ratchet) UnpickleLibOlm(value []byte, includesChainIndex bool) (int, error) { + //read ratchet data + curPos := 0 + readBytes, err := r.RootKey.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + curPos += readBytes + countSenderChains, readBytes, err := libolmpickle.UnpickleUInt32(value[curPos:]) //Length of sender chain + if err != nil { + return 0, err + } + curPos += readBytes + for i := uint32(0); i < countSenderChains; i++ { + if i == 0 { + //only first is stored + readBytes, err := r.SenderChains.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + r.SenderChains.IsSet = true + } else { + dummy := senderChain{} + readBytes, err := dummy.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + } + } + countReceivChains, readBytes, err := libolmpickle.UnpickleUInt32(value[curPos:]) //Length of recevier chain + if err != nil { + return 0, err + } + curPos += readBytes + r.ReceiverChains = make([]receiverChain, countReceivChains) + for i := uint32(0); i < countReceivChains; i++ { + readBytes, err := r.ReceiverChains[i].UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + } + countSkippedMessageKeys, readBytes, err := libolmpickle.UnpickleUInt32(value[curPos:]) //Length of skippedMessageKeys + if err != nil { + return 0, err + } + curPos += readBytes + r.SkippedMessageKeys = make([]skippedMessageKey, countSkippedMessageKeys) + for i := uint32(0); i < countSkippedMessageKeys; i++ { + readBytes, err := r.SkippedMessageKeys[i].UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + } + // pickle v 0x80000001 includes a chain index; pickle v1 does not. + if includesChainIndex { + _, readBytes, err := libolmpickle.UnpickleUInt32(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + } + return curPos, nil +} + +// PickleLibOlm encodes the ratchet into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (r Ratchet) PickleLibOlm(target []byte) (int, error) { + if len(target) < r.PickleLen() { + return 0, fmt.Errorf("pickle ratchet: %w", goolm.ErrValueTooShort) + } + written, err := r.RootKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle ratchet: %w", err) + } + if r.SenderChains.IsSet { + written += libolmpickle.PickleUInt32(1, target[written:]) //Length of sender chain, always 1 + writtenSender, err := r.SenderChains.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle ratchet: %w", err) + } + written += writtenSender + } else { + written += libolmpickle.PickleUInt32(0, target[written:]) //Length of sender chain + } + written += libolmpickle.PickleUInt32(uint32(len(r.ReceiverChains)), target[written:]) + for _, curChain := range r.ReceiverChains { + writtenChain, err := curChain.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle ratchet: %w", err) + } + written += writtenChain + } + written += libolmpickle.PickleUInt32(uint32(len(r.SkippedMessageKeys)), target[written:]) + for _, curChain := range r.SkippedMessageKeys { + writtenChain, err := curChain.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle ratchet: %w", err) + } + written += writtenChain + } + return written, nil +} + +// PickleLen returns the actual number of bytes the pickled ratchet will have. +func (r Ratchet) PickleLen() int { + length := r.RootKey.PickleLen() + if r.SenderChains.IsSet { + length += libolmpickle.PickleUInt32Len(1) + length += r.SenderChains.PickleLen() + } else { + length += libolmpickle.PickleUInt32Len(0) + } + length += libolmpickle.PickleUInt32Len(uint32(len(r.ReceiverChains))) + length += len(r.ReceiverChains) * receiverChain{}.PickleLen() + length += libolmpickle.PickleUInt32Len(uint32(len(r.SkippedMessageKeys))) + length += len(r.SkippedMessageKeys) * skippedMessageKey{}.PickleLen() + return length +} + +// PickleLen returns the minimum number of bytes the pickled ratchet must have. +func (r Ratchet) PickleLenMin() int { + length := r.RootKey.PickleLen() + length += libolmpickle.PickleUInt32Len(0) + length += libolmpickle.PickleUInt32Len(0) + length += libolmpickle.PickleUInt32Len(0) + return length +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/olm/skipped_message.go b/vendor/maunium.net/go/mautrix/crypto/goolm/olm/skipped_message.go new file mode 100644 index 0000000..944337f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/olm/skipped_message.go @@ -0,0 +1,55 @@ +package olm + +import ( + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +// skippedMessageKey stores a skipped message key +type skippedMessageKey struct { + RKey crypto.Curve25519PublicKey `json:"ratchet_key"` + MKey messageKey `json:"message_key"` +} + +// UnpickleLibOlm decodes the unencryted value and populates the chain accordingly. It returns the number of bytes read. +func (r *skippedMessageKey) UnpickleLibOlm(value []byte) (int, error) { + curPos := 0 + readBytes, err := r.RKey.UnpickleLibOlm(value) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = r.MKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + return curPos, nil +} + +// PickleLibOlm encodes the chain into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (r skippedMessageKey) PickleLibOlm(target []byte) (int, error) { + if len(target) < r.PickleLen() { + return 0, fmt.Errorf("pickle sender chain: %w", goolm.ErrValueTooShort) + } + written, err := r.RKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle sender chain: %w", err) + } + writtenChain, err := r.MKey.PickleLibOlm(target) + if err != nil { + return 0, fmt.Errorf("pickle sender chain: %w", err) + } + written += writtenChain + return written, nil +} + +// PickleLen returns the number of bytes the pickled chain will have. +func (r skippedMessageKey) PickleLen() int { + length := r.RKey.PickleLen() + length += r.MKey.PickleLen() + return length +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/pk/decryption.go b/vendor/maunium.net/go/mautrix/crypto/goolm/pk/decryption.go new file mode 100644 index 0000000..3fb3c2a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/pk/decryption.go @@ -0,0 +1,165 @@ +package pk + +import ( + "encoding/base64" + "errors" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/goolm/utilities" + "maunium.net/go/mautrix/id" +) + +const ( + decryptionPickleVersionJSON uint8 = 1 + decryptionPickleVersionLibOlm uint32 = 1 +) + +// Decryption is used to decrypt pk messages +type Decryption struct { + KeyPair crypto.Curve25519KeyPair `json:"key_pair"` +} + +// NewDecryption returns a new Decryption with a new generated key pair. +func NewDecryption() (*Decryption, error) { + keyPair, err := crypto.Curve25519GenerateKey(nil) + if err != nil { + return nil, err + } + return &Decryption{ + KeyPair: keyPair, + }, nil +} + +// NewDescriptionFromPrivate resturns a new Decryption with the private key fixed. +func NewDecryptionFromPrivate(privateKey crypto.Curve25519PrivateKey) (*Decryption, error) { + s := &Decryption{} + keyPair, err := crypto.Curve25519GenerateFromPrivate(privateKey) + if err != nil { + return nil, err + } + s.KeyPair = keyPair + return s, nil +} + +// PubKey returns the public key base 64 encoded. +func (s Decryption) PubKey() id.Curve25519 { + return s.KeyPair.B64Encoded() +} + +// PrivateKey returns the private key. +func (s Decryption) PrivateKey() crypto.Curve25519PrivateKey { + return s.KeyPair.PrivateKey +} + +// Decrypt decrypts the ciphertext and verifies the MAC. The base64 encoded key is used to construct the shared secret. +func (s Decryption) Decrypt(ciphertext, mac []byte, key id.Curve25519) ([]byte, error) { + keyDecoded, err := base64.RawStdEncoding.DecodeString(string(key)) + if err != nil { + return nil, err + } + sharedSecret, err := s.KeyPair.SharedSecret(keyDecoded) + if err != nil { + return nil, err + } + decodedMAC, err := goolm.Base64Decode(mac) + if err != nil { + return nil, err + } + cipher := cipher.NewAESSHA256(nil) + verified, err := cipher.Verify(sharedSecret, ciphertext, decodedMAC) + if err != nil { + return nil, err + } + if !verified { + return nil, fmt.Errorf("decrypt: %w", goolm.ErrBadMAC) + } + plaintext, err := cipher.Decrypt(sharedSecret, ciphertext) + if err != nil { + return nil, err + } + return plaintext, nil +} + +// PickleAsJSON returns an Decryption as a base64 string encrypted using the supplied key. The unencrypted representation of the Account is in JSON format. +func (a Decryption) PickleAsJSON(key []byte) ([]byte, error) { + return utilities.PickleAsJSON(a, decryptionPickleVersionJSON, key) +} + +// UnpickleAsJSON updates an Decryption by a base64 encrypted string using the supplied key. The unencrypted representation has to be in JSON format. +func (a *Decryption) UnpickleAsJSON(pickled, key []byte) error { + return utilities.UnpickleAsJSON(a, pickled, key, decryptionPickleVersionJSON) +} + +// Unpickle decodes the base64 encoded string and decrypts the result with the key. +// The decrypted value is then passed to UnpickleLibOlm. +func (a *Decryption) Unpickle(pickled, key []byte) error { + decrypted, err := cipher.Unpickle(key, pickled) + if err != nil { + return err + } + _, err = a.UnpickleLibOlm(decrypted) + return err +} + +// UnpickleLibOlm decodes the unencryted value and populates the Decryption accordingly. It returns the number of bytes read. +func (a *Decryption) UnpickleLibOlm(value []byte) (int, error) { + //First 4 bytes are the accountPickleVersion + pickledVersion, curPos, err := libolmpickle.UnpickleUInt32(value) + if err != nil { + return 0, err + } + switch pickledVersion { + case decryptionPickleVersionLibOlm: + default: + return 0, fmt.Errorf("unpickle olmSession: %w", goolm.ErrBadVersion) + } + readBytes, err := a.KeyPair.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + return curPos, nil +} + +// Pickle returns a base64 encoded and with key encrypted pickled Decryption using PickleLibOlm(). +func (a Decryption) Pickle(key []byte) ([]byte, error) { + pickeledBytes := make([]byte, a.PickleLen()) + written, err := a.PickleLibOlm(pickeledBytes) + if err != nil { + return nil, err + } + if written != len(pickeledBytes) { + return nil, errors.New("number of written bytes not correct") + } + encrypted, err := cipher.Pickle(key, pickeledBytes) + if err != nil { + return nil, err + } + return encrypted, nil +} + +// PickleLibOlm encodes the Decryption into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (a Decryption) PickleLibOlm(target []byte) (int, error) { + if len(target) < a.PickleLen() { + return 0, fmt.Errorf("pickle Decryption: %w", goolm.ErrValueTooShort) + } + written := libolmpickle.PickleUInt32(decryptionPickleVersionLibOlm, target) + writtenKey, err := a.KeyPair.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle Decryption: %w", err) + } + written += writtenKey + return written, nil +} + +// PickleLen returns the number of bytes the pickled Decryption will have. +func (a Decryption) PickleLen() int { + length := libolmpickle.PickleUInt32Len(decryptionPickleVersionLibOlm) + length += a.KeyPair.PickleLen() + return length +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/pk/encryption.go b/vendor/maunium.net/go/mautrix/crypto/goolm/pk/encryption.go new file mode 100644 index 0000000..dc50a6b --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/pk/encryption.go @@ -0,0 +1,49 @@ +package pk + +import ( + "encoding/base64" + + "maunium.net/go/mautrix/id" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +// Encryption is used to encrypt pk messages +type Encryption struct { + RecipientKey crypto.Curve25519PublicKey `json:"recipient_key"` +} + +// NewEncryption returns a new Encryption with the base64 encoded public key of the recipient +func NewEncryption(pubKey id.Curve25519) (*Encryption, error) { + pubKeyDecoded, err := base64.RawStdEncoding.DecodeString(string(pubKey)) + if err != nil { + return nil, err + } + return &Encryption{ + RecipientKey: pubKeyDecoded, + }, nil +} + +// Encrypt encrypts the plaintext with the privateKey and returns the ciphertext and base64 encoded MAC. +func (e Encryption) Encrypt(plaintext []byte, privateKey crypto.Curve25519PrivateKey) (ciphertext, mac []byte, err error) { + keyPair, err := crypto.Curve25519GenerateFromPrivate(privateKey) + if err != nil { + return nil, nil, err + } + sharedSecret, err := keyPair.SharedSecret(e.RecipientKey) + if err != nil { + return nil, nil, err + } + cipher := cipher.NewAESSHA256(nil) + ciphertext, err = cipher.Encrypt(sharedSecret, plaintext) + if err != nil { + return nil, nil, err + } + mac, err = cipher.MAC(sharedSecret, ciphertext) + if err != nil { + return nil, nil, err + } + return ciphertext, goolm.Base64Encode(mac), nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/pk/signing.go b/vendor/maunium.net/go/mautrix/crypto/goolm/pk/signing.go new file mode 100644 index 0000000..046838f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/pk/signing.go @@ -0,0 +1,44 @@ +package pk + +import ( + "crypto/rand" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/id" +) + +// Signing is used for signing a pk +type Signing struct { + KeyPair crypto.Ed25519KeyPair `json:"key_pair"` + Seed []byte `json:"seed"` +} + +// NewSigningFromSeed constructs a new Signing based on a seed. +func NewSigningFromSeed(seed []byte) (*Signing, error) { + s := &Signing{} + s.Seed = seed + s.KeyPair = crypto.Ed25519GenerateFromSeed(seed) + return s, nil +} + +// NewSigning returns a Signing based on a random seed +func NewSigning() (*Signing, error) { + seed := make([]byte, 32) + _, err := rand.Read(seed) + if err != nil { + return nil, err + } + return NewSigningFromSeed(seed) +} + +// Sign returns the signature of the message base64 encoded. +func (s Signing) Sign(message []byte) []byte { + signature := s.KeyPair.Sign(message) + return goolm.Base64Encode(signature) +} + +// PublicKey returns the public key of the key pair base 64 encoded. +func (s Signing) PublicKey() id.Ed25519 { + return s.KeyPair.B64Encoded() +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/sas/main.go b/vendor/maunium.net/go/mautrix/crypto/goolm/sas/main.go new file mode 100644 index 0000000..7337d5f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/sas/main.go @@ -0,0 +1,76 @@ +// sas provides the means to do SAS between keys +package sas + +import ( + "io" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/crypto" +) + +// SAS contains the key pair and secret for SAS. +type SAS struct { + KeyPair crypto.Curve25519KeyPair + Secret []byte +} + +// New creates a new SAS with a new key pair. +func New() (*SAS, error) { + kp, err := crypto.Curve25519GenerateKey(nil) + if err != nil { + return nil, err + } + s := &SAS{ + KeyPair: kp, + } + return s, nil +} + +// GetPubkey returns the public key of the key pair base64 encoded +func (s SAS) GetPubkey() []byte { + return goolm.Base64Encode(s.KeyPair.PublicKey) +} + +// SetTheirKey sets the key of the other party and computes the shared secret. +func (s *SAS) SetTheirKey(key []byte) error { + keyDecoded, err := goolm.Base64Decode(key) + if err != nil { + return err + } + sharedSecret, err := s.KeyPair.SharedSecret(keyDecoded) + if err != nil { + return err + } + s.Secret = sharedSecret + return nil +} + +// GenerateBytes creates length bytes from the shared secret and info. +func (s SAS) GenerateBytes(info []byte, length uint) ([]byte, error) { + byteReader := crypto.HKDFSHA256(s.Secret, nil, info) + output := make([]byte, length) + if _, err := io.ReadFull(byteReader, output); err != nil { + return nil, err + } + return output, nil +} + +// calculateMAC returns a base64 encoded MAC of input. +func (s *SAS) calculateMAC(input, info []byte, length uint) ([]byte, error) { + key, err := s.GenerateBytes(info, length) + if err != nil { + return nil, err + } + mac := crypto.HMACSHA256(key, input) + return goolm.Base64Encode(mac), nil +} + +// CalculateMACFixes returns a base64 encoded, 32 byte long MAC of input. +func (s SAS) CalculateMAC(input, info []byte) ([]byte, error) { + return s.calculateMAC(input, info, 32) +} + +// CalculateMACLongKDF returns a base64 encoded, 256 byte long MAC of input. +func (s SAS) CalculateMACLongKDF(input, info []byte) ([]byte, error) { + return s.calculateMAC(input, info, 256) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/session/main.go b/vendor/maunium.net/go/mautrix/crypto/goolm/session/main.go new file mode 100644 index 0000000..0caf804 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/session/main.go @@ -0,0 +1,2 @@ +// session provides the different types of sessions for en/decrypting of messages +package session diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/session/megolm_inbound_session.go b/vendor/maunium.net/go/mautrix/crypto/goolm/session/megolm_inbound_session.go new file mode 100644 index 0000000..165f7f1 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/session/megolm_inbound_session.go @@ -0,0 +1,276 @@ +package session + +import ( + "encoding/base64" + "errors" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/goolm/megolm" + "maunium.net/go/mautrix/crypto/goolm/message" + "maunium.net/go/mautrix/crypto/goolm/utilities" + "maunium.net/go/mautrix/id" +) + +const ( + megolmInboundSessionPickleVersionJSON byte = 1 + megolmInboundSessionPickleVersionLibOlm uint32 = 2 +) + +// MegolmInboundSession stores information about the sessions of receive. +type MegolmInboundSession struct { + Ratchet megolm.Ratchet `json:"ratchet"` + SigningKey crypto.Ed25519PublicKey `json:"signing_key"` + InitialRatchet megolm.Ratchet `json:"initial_ratchet"` + SigningKeyVerified bool `json:"signing_key_verified"` //not used for now +} + +// NewMegolmInboundSession creates a new MegolmInboundSession from a base64 encoded session sharing message. +func NewMegolmInboundSession(input []byte) (*MegolmInboundSession, error) { + var err error + input, err = goolm.Base64Decode(input) + if err != nil { + return nil, err + } + msg := message.MegolmSessionSharing{} + err = msg.VerifyAndDecode(input) + if err != nil { + return nil, err + } + o := &MegolmInboundSession{} + o.SigningKey = msg.PublicKey + o.SigningKeyVerified = true + ratchet, err := megolm.New(msg.Counter, msg.RatchetData) + if err != nil { + return nil, err + } + o.Ratchet = *ratchet + o.InitialRatchet = *ratchet + return o, nil +} + +// NewMegolmInboundSessionFromExport creates a new MegolmInboundSession from a base64 encoded session export message. +func NewMegolmInboundSessionFromExport(input []byte) (*MegolmInboundSession, error) { + var err error + input, err = goolm.Base64Decode(input) + if err != nil { + return nil, err + } + msg := message.MegolmSessionExport{} + err = msg.Decode(input) + if err != nil { + return nil, err + } + o := &MegolmInboundSession{} + o.SigningKey = msg.PublicKey + ratchet, err := megolm.New(msg.Counter, msg.RatchetData) + if err != nil { + return nil, err + } + o.Ratchet = *ratchet + o.InitialRatchet = *ratchet + return o, nil +} + +// MegolmInboundSessionFromPickled loads the MegolmInboundSession details from a pickled base64 string. The input is decrypted with the supplied key. +func MegolmInboundSessionFromPickled(pickled, key []byte) (*MegolmInboundSession, error) { + if len(pickled) == 0 { + return nil, fmt.Errorf("megolmInboundSessionFromPickled: %w", goolm.ErrEmptyInput) + } + a := &MegolmInboundSession{} + err := a.Unpickle(pickled, key) + if err != nil { + return nil, err + } + return a, nil +} + +// getRatchet tries to find the correct ratchet for a messageIndex. +func (o MegolmInboundSession) getRatchet(messageIndex uint32) (*megolm.Ratchet, error) { + // pick a megolm instance to use. if we are at or beyond the latest ratchet value, use that + if (messageIndex - o.Ratchet.Counter) < uint32(1<<31) { + o.Ratchet.AdvanceTo(messageIndex) + return &o.Ratchet, nil + } + if (messageIndex - o.InitialRatchet.Counter) >= uint32(1<<31) { + // the counter is before our initial ratchet - we can't decode this + return nil, fmt.Errorf("decrypt: %w", goolm.ErrRatchetNotAvailable) + } + // otherwise, start from the initial ratchet. Take a copy so that we don't overwrite the initial ratchet + copiedRatchet := o.InitialRatchet + copiedRatchet.AdvanceTo(messageIndex) + return &copiedRatchet, nil + +} + +// Decrypt decrypts a base64 encoded group message. +func (o *MegolmInboundSession) Decrypt(ciphertext []byte) ([]byte, uint32, error) { + if o.SigningKey == nil { + return nil, 0, fmt.Errorf("decrypt: %w", goolm.ErrBadMessageFormat) + } + decoded, err := goolm.Base64Decode(ciphertext) + if err != nil { + return nil, 0, err + } + msg := &message.GroupMessage{} + err = msg.Decode(decoded) + if err != nil { + return nil, 0, err + } + if msg.Version != protocolVersion { + return nil, 0, fmt.Errorf("decrypt: %w", goolm.ErrWrongProtocolVersion) + } + if msg.Ciphertext == nil || !msg.HasMessageIndex { + return nil, 0, fmt.Errorf("decrypt: %w", goolm.ErrBadMessageFormat) + } + + // verify signature + verifiedSignature := msg.VerifySignatureInline(o.SigningKey, decoded) + if !verifiedSignature { + return nil, 0, fmt.Errorf("decrypt: %w", goolm.ErrBadSignature) + } + + targetRatch, err := o.getRatchet(msg.MessageIndex) + if err != nil { + return nil, 0, err + } + + decrypted, err := targetRatch.Decrypt(decoded, &o.SigningKey, msg) + if err != nil { + return nil, 0, err + } + o.SigningKeyVerified = true + return decrypted, msg.MessageIndex, nil + +} + +// SessionID returns the base64 endoded signing key +func (o MegolmInboundSession) SessionID() id.SessionID { + return id.SessionID(base64.RawStdEncoding.EncodeToString(o.SigningKey)) +} + +// PickleAsJSON returns an MegolmInboundSession as a base64 string encrypted using the supplied key. The unencrypted representation of the Account is in JSON format. +func (o MegolmInboundSession) PickleAsJSON(key []byte) ([]byte, error) { + return utilities.PickleAsJSON(o, megolmInboundSessionPickleVersionJSON, key) +} + +// UnpickleAsJSON updates an MegolmInboundSession by a base64 encrypted string using the supplied key. The unencrypted representation has to be in JSON format. +func (o *MegolmInboundSession) UnpickleAsJSON(pickled, key []byte) error { + return utilities.UnpickleAsJSON(o, pickled, key, megolmInboundSessionPickleVersionJSON) +} + +// SessionExportMessage creates an base64 encoded export of the session. +func (o MegolmInboundSession) SessionExportMessage(messageIndex uint32) ([]byte, error) { + ratchet, err := o.getRatchet(messageIndex) + if err != nil { + return nil, err + } + return ratchet.SessionExportMessage(o.SigningKey) +} + +// Unpickle decodes the base64 encoded string and decrypts the result with the key. +// The decrypted value is then passed to UnpickleLibOlm. +func (o *MegolmInboundSession) Unpickle(pickled, key []byte) error { + decrypted, err := cipher.Unpickle(key, pickled) + if err != nil { + return err + } + _, err = o.UnpickleLibOlm(decrypted) + return err +} + +// UnpickleLibOlm decodes the unencryted value and populates the Session accordingly. It returns the number of bytes read. +func (o *MegolmInboundSession) UnpickleLibOlm(value []byte) (int, error) { + //First 4 bytes are the accountPickleVersion + pickledVersion, curPos, err := libolmpickle.UnpickleUInt32(value) + if err != nil { + return 0, err + } + switch pickledVersion { + case megolmInboundSessionPickleVersionLibOlm, 1: + default: + return 0, fmt.Errorf("unpickle MegolmInboundSession: %w", goolm.ErrBadVersion) + } + readBytes, err := o.InitialRatchet.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = o.Ratchet.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = o.SigningKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + if pickledVersion == 1 { + // pickle v1 had no signing_key_verified field (all keyshares were verified at import time) + o.SigningKeyVerified = true + } else { + o.SigningKeyVerified, readBytes, err = libolmpickle.UnpickleBool(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + } + return curPos, nil +} + +// Pickle returns a base64 encoded and with key encrypted pickled MegolmInboundSession using PickleLibOlm(). +func (o MegolmInboundSession) Pickle(key []byte) ([]byte, error) { + pickeledBytes := make([]byte, o.PickleLen()) + written, err := o.PickleLibOlm(pickeledBytes) + if err != nil { + return nil, err + } + if written != len(pickeledBytes) { + return nil, errors.New("number of written bytes not correct") + } + encrypted, err := cipher.Pickle(key, pickeledBytes) + if err != nil { + return nil, err + } + return encrypted, nil +} + +// PickleLibOlm encodes the session into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (o MegolmInboundSession) PickleLibOlm(target []byte) (int, error) { + if len(target) < o.PickleLen() { + return 0, fmt.Errorf("pickle MegolmInboundSession: %w", goolm.ErrValueTooShort) + } + written := libolmpickle.PickleUInt32(megolmInboundSessionPickleVersionLibOlm, target) + writtenInitRatchet, err := o.InitialRatchet.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmInboundSession: %w", err) + } + written += writtenInitRatchet + writtenRatchet, err := o.Ratchet.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmInboundSession: %w", err) + } + written += writtenRatchet + writtenPubKey, err := o.SigningKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmInboundSession: %w", err) + } + written += writtenPubKey + written += libolmpickle.PickleBool(o.SigningKeyVerified, target[written:]) + return written, nil +} + +// PickleLen returns the number of bytes the pickled session will have. +func (o MegolmInboundSession) PickleLen() int { + length := libolmpickle.PickleUInt32Len(megolmInboundSessionPickleVersionLibOlm) + length += o.InitialRatchet.PickleLen() + length += o.Ratchet.PickleLen() + length += o.SigningKey.PickleLen() + length += libolmpickle.PickleBoolLen(o.SigningKeyVerified) + return length +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/session/megolm_outbound_session.go b/vendor/maunium.net/go/mautrix/crypto/goolm/session/megolm_outbound_session.go new file mode 100644 index 0000000..e594258 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/session/megolm_outbound_session.go @@ -0,0 +1,171 @@ +package session + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + + "maunium.net/go/mautrix/id" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/goolm/megolm" + "maunium.net/go/mautrix/crypto/goolm/utilities" +) + +const ( + megolmOutboundSessionPickleVersion byte = 1 + megolmOutboundSessionPickleVersionLibOlm uint32 = 1 +) + +// MegolmOutboundSession stores information about the sessions to send. +type MegolmOutboundSession struct { + Ratchet megolm.Ratchet `json:"ratchet"` + SigningKey crypto.Ed25519KeyPair `json:"signing_key"` +} + +// NewMegolmOutboundSession creates a new MegolmOutboundSession. +func NewMegolmOutboundSession() (*MegolmOutboundSession, error) { + o := &MegolmOutboundSession{} + var err error + o.SigningKey, err = crypto.Ed25519GenerateKey(nil) + if err != nil { + return nil, err + } + var randomData [megolm.RatchetParts * megolm.RatchetPartLength]byte + _, err = rand.Read(randomData[:]) + if err != nil { + return nil, err + } + ratchet, err := megolm.New(0, randomData) + if err != nil { + return nil, err + } + o.Ratchet = *ratchet + return o, nil +} + +// MegolmOutboundSessionFromPickled loads the MegolmOutboundSession details from a pickled base64 string. The input is decrypted with the supplied key. +func MegolmOutboundSessionFromPickled(pickled, key []byte) (*MegolmOutboundSession, error) { + if len(pickled) == 0 { + return nil, fmt.Errorf("megolmOutboundSessionFromPickled: %w", goolm.ErrEmptyInput) + } + a := &MegolmOutboundSession{} + err := a.Unpickle(pickled, key) + if err != nil { + return nil, err + } + return a, nil +} + +// Encrypt encrypts the plaintext as a base64 encoded group message. +func (o *MegolmOutboundSession) Encrypt(plaintext []byte) ([]byte, error) { + encrypted, err := o.Ratchet.Encrypt(plaintext, &o.SigningKey) + if err != nil { + return nil, err + } + return goolm.Base64Encode(encrypted), nil +} + +// SessionID returns the base64 endoded public signing key +func (o MegolmOutboundSession) SessionID() id.SessionID { + return id.SessionID(base64.RawStdEncoding.EncodeToString(o.SigningKey.PublicKey)) +} + +// PickleAsJSON returns an Session as a base64 string encrypted using the supplied key. The unencrypted representation of the Account is in JSON format. +func (o MegolmOutboundSession) PickleAsJSON(key []byte) ([]byte, error) { + return utilities.PickleAsJSON(o, megolmOutboundSessionPickleVersion, key) +} + +// UnpickleAsJSON updates an Session by a base64 encrypted string with the key. The unencrypted representation has to be in JSON format. +func (o *MegolmOutboundSession) UnpickleAsJSON(pickled, key []byte) error { + return utilities.UnpickleAsJSON(o, pickled, key, megolmOutboundSessionPickleVersion) +} + +// Unpickle decodes the base64 encoded string and decrypts the result with the key. +// The decrypted value is then passed to UnpickleLibOlm. +func (o *MegolmOutboundSession) Unpickle(pickled, key []byte) error { + decrypted, err := cipher.Unpickle(key, pickled) + if err != nil { + return err + } + _, err = o.UnpickleLibOlm(decrypted) + return err +} + +// UnpickleLibOlm decodes the unencryted value and populates the Session accordingly. It returns the number of bytes read. +func (o *MegolmOutboundSession) UnpickleLibOlm(value []byte) (int, error) { + //First 4 bytes are the accountPickleVersion + pickledVersion, curPos, err := libolmpickle.UnpickleUInt32(value) + if err != nil { + return 0, err + } + switch pickledVersion { + case megolmOutboundSessionPickleVersionLibOlm: + default: + return 0, fmt.Errorf("unpickle MegolmInboundSession: %w", goolm.ErrBadVersion) + } + readBytes, err := o.Ratchet.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = o.SigningKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + return curPos, nil +} + +// Pickle returns a base64 encoded and with key encrypted pickled MegolmOutboundSession using PickleLibOlm(). +func (o MegolmOutboundSession) Pickle(key []byte) ([]byte, error) { + pickeledBytes := make([]byte, o.PickleLen()) + written, err := o.PickleLibOlm(pickeledBytes) + if err != nil { + return nil, err + } + if written != len(pickeledBytes) { + return nil, errors.New("number of written bytes not correct") + } + encrypted, err := cipher.Pickle(key, pickeledBytes) + if err != nil { + return nil, err + } + return encrypted, nil +} + +// PickleLibOlm encodes the session into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (o MegolmOutboundSession) PickleLibOlm(target []byte) (int, error) { + if len(target) < o.PickleLen() { + return 0, fmt.Errorf("pickle MegolmOutboundSession: %w", goolm.ErrValueTooShort) + } + written := libolmpickle.PickleUInt32(megolmOutboundSessionPickleVersionLibOlm, target) + writtenRatchet, err := o.Ratchet.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmOutboundSession: %w", err) + } + written += writtenRatchet + writtenPubKey, err := o.SigningKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmOutboundSession: %w", err) + } + written += writtenPubKey + return written, nil +} + +// PickleLen returns the number of bytes the pickled session will have. +func (o MegolmOutboundSession) PickleLen() int { + length := libolmpickle.PickleUInt32Len(megolmOutboundSessionPickleVersionLibOlm) + length += o.Ratchet.PickleLen() + length += o.SigningKey.PickleLen() + return length +} + +func (o MegolmOutboundSession) SessionSharingMessage() ([]byte, error) { + return o.Ratchet.SessionSharingMessage(o.SigningKey) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/session/olm_session.go b/vendor/maunium.net/go/mautrix/crypto/goolm/session/olm_session.go new file mode 100644 index 0000000..6655e0a --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/session/olm_session.go @@ -0,0 +1,476 @@ +package session + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/crypto/goolm/libolmpickle" + "maunium.net/go/mautrix/crypto/goolm/message" + "maunium.net/go/mautrix/crypto/goolm/olm" + "maunium.net/go/mautrix/crypto/goolm/utilities" + "maunium.net/go/mautrix/id" +) + +const ( + olmSessionPickleVersionJSON uint8 = 1 + olmSessionPickleVersionLibOlm uint32 = 1 +) + +const ( + protocolVersion = 0x3 +) + +// OlmSession stores all information for an olm session +type OlmSession struct { + ReceivedMessage bool `json:"received_message"` + AliceIdentityKey crypto.Curve25519PublicKey `json:"alice_id_key"` + AliceBaseKey crypto.Curve25519PublicKey `json:"alice_base_key"` + BobOneTimeKey crypto.Curve25519PublicKey `json:"bob_one_time_key"` + Ratchet olm.Ratchet `json:"ratchet"` +} + +// SearchOTKFunc is used to retrieve a crypto.OneTimeKey from a public key. +type SearchOTKFunc = func(crypto.Curve25519PublicKey) *crypto.OneTimeKey + +// OlmSessionFromJSONPickled loads an OlmSession from a pickled base64 string. Decrypts +// the Session using the supplied key. +func OlmSessionFromJSONPickled(pickled, key []byte) (*OlmSession, error) { + if len(pickled) == 0 { + return nil, fmt.Errorf("sessionFromPickled: %w", goolm.ErrEmptyInput) + } + a := &OlmSession{} + err := a.UnpickleAsJSON(pickled, key) + if err != nil { + return nil, err + } + return a, nil +} + +// OlmSessionFromPickled loads the OlmSession details from a pickled base64 string. The input is decrypted with the supplied key. +func OlmSessionFromPickled(pickled, key []byte) (*OlmSession, error) { + if len(pickled) == 0 { + return nil, fmt.Errorf("sessionFromPickled: %w", goolm.ErrEmptyInput) + } + a := &OlmSession{} + err := a.Unpickle(pickled, key) + if err != nil { + return nil, err + } + return a, nil +} + +// NewOlmSession creates a new Session. +func NewOlmSession() *OlmSession { + s := &OlmSession{} + s.Ratchet = *olm.New() + return s +} + +// NewOutboundOlmSession creates a new outbound session for sending the first message to a +// given curve25519 identityKey and oneTimeKey. +func NewOutboundOlmSession(identityKeyAlice crypto.Curve25519KeyPair, identityKeyBob crypto.Curve25519PublicKey, oneTimeKeyBob crypto.Curve25519PublicKey) (*OlmSession, error) { + s := NewOlmSession() + //generate E_A + baseKey, err := crypto.Curve25519GenerateKey(nil) + if err != nil { + return nil, err + } + //generate T_0 + ratchetKey, err := crypto.Curve25519GenerateKey(nil) + if err != nil { + return nil, err + } + + //Calculate shared secret via Triple Diffie-Hellman + var secret []byte + //ECDH(I_A,E_B) + idSecret, err := identityKeyAlice.SharedSecret(oneTimeKeyBob) + if err != nil { + return nil, err + } + //ECDH(E_A,I_B) + baseIdSecret, err := baseKey.SharedSecret(identityKeyBob) + if err != nil { + return nil, err + } + //ECDH(E_A,E_B) + baseOneTimeSecret, err := baseKey.SharedSecret(oneTimeKeyBob) + if err != nil { + return nil, err + } + secret = append(secret, idSecret...) + secret = append(secret, baseIdSecret...) + secret = append(secret, baseOneTimeSecret...) + //Init Ratchet + s.Ratchet.InitializeAsAlice(secret, ratchetKey) + s.AliceIdentityKey = identityKeyAlice.PublicKey + s.AliceBaseKey = baseKey.PublicKey + s.BobOneTimeKey = oneTimeKeyBob + return s, nil +} + +// NewInboundOlmSession creates a new inbound session from receiving the first message. +func NewInboundOlmSession(identityKeyAlice *crypto.Curve25519PublicKey, receivedOTKMsg []byte, searchBobOTK SearchOTKFunc, identityKeyBob crypto.Curve25519KeyPair) (*OlmSession, error) { + decodedOTKMsg, err := goolm.Base64Decode(receivedOTKMsg) + if err != nil { + return nil, err + } + s := NewOlmSession() + + //decode OneTimeKeyMessage + oneTimeMsg := message.PreKeyMessage{} + err = oneTimeMsg.Decode(decodedOTKMsg) + if err != nil { + return nil, fmt.Errorf("OneTimeKeyMessage decode: %w", err) + } + if !oneTimeMsg.CheckFields(identityKeyAlice) { + return nil, fmt.Errorf("OneTimeKeyMessage check fields: %w", goolm.ErrBadMessageFormat) + } + + //Either the identityKeyAlice is set and/or the oneTimeMsg.IdentityKey is set, which is checked + // by oneTimeMsg.CheckFields + if identityKeyAlice != nil && len(oneTimeMsg.IdentityKey) != 0 { + //if both are set, compare them + if !identityKeyAlice.Equal(oneTimeMsg.IdentityKey) { + return nil, fmt.Errorf("OneTimeKeyMessage identity keys: %w", goolm.ErrBadMessageKeyID) + } + } + if identityKeyAlice == nil { + //for downstream use set + identityKeyAlice = &oneTimeMsg.IdentityKey + } + + oneTimeKeyBob := searchBobOTK(oneTimeMsg.OneTimeKey) + if oneTimeKeyBob == nil { + return nil, fmt.Errorf("ourOneTimeKey: %w", goolm.ErrBadMessageKeyID) + } + + //Calculate shared secret via Triple Diffie-Hellman + var secret []byte + //ECDH(E_B,I_A) + idSecret, err := oneTimeKeyBob.Key.SharedSecret(*identityKeyAlice) + if err != nil { + return nil, err + } + //ECDH(I_B,E_A) + baseIdSecret, err := identityKeyBob.SharedSecret(oneTimeMsg.BaseKey) + if err != nil { + return nil, err + } + //ECDH(E_B,E_A) + baseOneTimeSecret, err := oneTimeKeyBob.Key.SharedSecret(oneTimeMsg.BaseKey) + if err != nil { + return nil, err + } + secret = append(secret, idSecret...) + secret = append(secret, baseIdSecret...) + secret = append(secret, baseOneTimeSecret...) + //decode message + msg := message.Message{} + err = msg.Decode(oneTimeMsg.Message) + if err != nil { + return nil, fmt.Errorf("Message decode: %w", err) + } + + if len(msg.RatchetKey) == 0 { + return nil, fmt.Errorf("Message missing ratchet key: %w", goolm.ErrBadMessageFormat) + } + //Init Ratchet + s.Ratchet.InitializeAsBob(secret, msg.RatchetKey) + s.AliceBaseKey = oneTimeMsg.BaseKey + s.AliceIdentityKey = oneTimeMsg.IdentityKey + s.BobOneTimeKey = oneTimeKeyBob.Key.PublicKey + + //https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/olm.md states to remove the oneTimeKey + //this is done via the account itself + return s, nil +} + +// PickleAsJSON returns an Session as a base64 string encrypted using the supplied key. The unencrypted representation of the Account is in JSON format. +func (a OlmSession) PickleAsJSON(key []byte) ([]byte, error) { + return utilities.PickleAsJSON(a, olmSessionPickleVersionJSON, key) +} + +// UnpickleAsJSON updates an Session by a base64 encrypted string with the key. The unencrypted representation has to be in JSON format. +func (a *OlmSession) UnpickleAsJSON(pickled, key []byte) error { + return utilities.UnpickleAsJSON(a, pickled, key, olmSessionPickleVersionJSON) +} + +// ID returns an identifier for this Session. Will be the same for both ends of the conversation. +// Generated by hashing the public keys used to create the session. +func (s OlmSession) ID() id.SessionID { + message := make([]byte, 3*crypto.Curve25519KeyLength) + copy(message, s.AliceIdentityKey) + copy(message[crypto.Curve25519KeyLength:], s.AliceBaseKey) + copy(message[2*crypto.Curve25519KeyLength:], s.BobOneTimeKey) + hash := crypto.SHA256(message) + res := id.SessionID(goolm.Base64Encode(hash)) + return res +} + +// HasReceivedMessage returns true if this session has received any message. +func (s OlmSession) HasReceivedMessage() bool { + return s.ReceivedMessage +} + +// MatchesInboundSessionFrom checks if the oneTimeKeyMsg message is set for this inbound +// Session. This can happen if multiple messages are sent to this Account +// before this Account sends a message in reply. Returns true if the session +// matches. Returns false if the session does not match. +func (s OlmSession) MatchesInboundSessionFrom(theirIdentityKeyEncoded *id.Curve25519, receivedOTKMsg []byte) (bool, error) { + if len(receivedOTKMsg) == 0 { + return false, fmt.Errorf("inbound match: %w", goolm.ErrEmptyInput) + } + decodedOTKMsg, err := goolm.Base64Decode(receivedOTKMsg) + if err != nil { + return false, err + } + + var theirIdentityKey *crypto.Curve25519PublicKey + if theirIdentityKeyEncoded != nil { + decodedKey, err := base64.RawStdEncoding.DecodeString(string(*theirIdentityKeyEncoded)) + if err != nil { + return false, err + } + theirIdentityKeyByte := crypto.Curve25519PublicKey(decodedKey) + theirIdentityKey = &theirIdentityKeyByte + } + + msg := message.PreKeyMessage{} + err = msg.Decode(decodedOTKMsg) + if err != nil { + return false, err + } + if !msg.CheckFields(theirIdentityKey) { + return false, nil + } + + same := true + if msg.IdentityKey != nil { + same = same && msg.IdentityKey.Equal(s.AliceIdentityKey) + } + if theirIdentityKey != nil { + same = same && theirIdentityKey.Equal(s.AliceIdentityKey) + } + same = same && bytes.Equal(msg.BaseKey, s.AliceBaseKey) + same = same && bytes.Equal(msg.OneTimeKey, s.BobOneTimeKey) + return same, nil +} + +// EncryptMsgType returns the type of the next message that Encrypt will +// return. Returns MsgTypePreKey if the message will be a oneTimeKeyMsg. +// Returns MsgTypeMsg if the message will be a normal message. +func (s OlmSession) EncryptMsgType() id.OlmMsgType { + if s.ReceivedMessage { + return id.OlmMsgTypeMsg + } + return id.OlmMsgTypePreKey +} + +// Encrypt encrypts a message using the Session. Returns the encrypted message base64 encoded. If reader is nil, crypto/rand is used for key generations. +func (s *OlmSession) Encrypt(plaintext []byte, reader io.Reader) (id.OlmMsgType, []byte, error) { + if len(plaintext) == 0 { + return 0, nil, fmt.Errorf("encrypt: %w", goolm.ErrEmptyInput) + } + messageType := s.EncryptMsgType() + encrypted, err := s.Ratchet.Encrypt(plaintext, reader) + if err != nil { + return 0, nil, err + } + result := encrypted + if !s.ReceivedMessage { + msg := message.PreKeyMessage{} + msg.Version = protocolVersion + msg.OneTimeKey = s.BobOneTimeKey + msg.IdentityKey = s.AliceIdentityKey + msg.BaseKey = s.AliceBaseKey + msg.Message = encrypted + + var err error + messageBody, err := msg.Encode() + if err != nil { + return 0, nil, err + } + result = messageBody + } + + return messageType, goolm.Base64Encode(result), nil +} + +// Decrypt decrypts a base64 encoded message using the Session. +func (s *OlmSession) Decrypt(crypttext []byte, msgType id.OlmMsgType) ([]byte, error) { + if len(crypttext) == 0 { + return nil, fmt.Errorf("decrypt: %w", goolm.ErrEmptyInput) + } + decodedCrypttext, err := goolm.Base64Decode(crypttext) + if err != nil { + return nil, err + } + msgBody := decodedCrypttext + if msgType != id.OlmMsgTypeMsg { + //Pre-Key Message + msg := message.PreKeyMessage{} + err := msg.Decode(decodedCrypttext) + if err != nil { + return nil, err + } + msgBody = msg.Message + } + plaintext, err := s.Ratchet.Decrypt(msgBody) + if err != nil { + return nil, err + } + s.ReceivedMessage = true + return plaintext, nil +} + +// Unpickle decodes the base64 encoded string and decrypts the result with the key. +// The decrypted value is then passed to UnpickleLibOlm. +func (o *OlmSession) Unpickle(pickled, key []byte) error { + decrypted, err := cipher.Unpickle(key, pickled) + if err != nil { + return err + } + _, err = o.UnpickleLibOlm(decrypted) + return err +} + +// UnpickleLibOlm decodes the unencryted value and populates the Session accordingly. It returns the number of bytes read. +func (o *OlmSession) UnpickleLibOlm(value []byte) (int, error) { + //First 4 bytes are the accountPickleVersion + pickledVersion, curPos, err := libolmpickle.UnpickleUInt32(value) + if err != nil { + return 0, err + } + includesChainIndex := true + switch pickledVersion { + case olmSessionPickleVersionLibOlm: + includesChainIndex = false + case uint32(0x80000001): + includesChainIndex = true + default: + return 0, fmt.Errorf("unpickle olmSession: %w", goolm.ErrBadVersion) + } + var readBytes int + o.ReceivedMessage, readBytes, err = libolmpickle.UnpickleBool(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = o.AliceIdentityKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = o.AliceBaseKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = o.BobOneTimeKey.UnpickleLibOlm(value[curPos:]) + if err != nil { + return 0, err + } + curPos += readBytes + readBytes, err = o.Ratchet.UnpickleLibOlm(value[curPos:], includesChainIndex) + if err != nil { + return 0, err + } + curPos += readBytes + return curPos, nil +} + +// Pickle returns a base64 encoded and with key encrypted pickled olmSession using PickleLibOlm(). +func (o OlmSession) Pickle(key []byte) ([]byte, error) { + pickeledBytes := make([]byte, o.PickleLen()) + written, err := o.PickleLibOlm(pickeledBytes) + if err != nil { + return nil, err + } + if written != len(pickeledBytes) { + return nil, errors.New("number of written bytes not correct") + } + encrypted, err := cipher.Pickle(key, pickeledBytes) + if err != nil { + return nil, err + } + return encrypted, nil +} + +// PickleLibOlm encodes the session into target. target has to have a size of at least PickleLen() and is written to from index 0. +// It returns the number of bytes written. +func (o OlmSession) PickleLibOlm(target []byte) (int, error) { + if len(target) < o.PickleLen() { + return 0, fmt.Errorf("pickle MegolmOutboundSession: %w", goolm.ErrValueTooShort) + } + written := libolmpickle.PickleUInt32(olmSessionPickleVersionLibOlm, target) + written += libolmpickle.PickleBool(o.ReceivedMessage, target[written:]) + writtenRatchet, err := o.AliceIdentityKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmOutboundSession: %w", err) + } + written += writtenRatchet + writtenRatchet, err = o.AliceBaseKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmOutboundSession: %w", err) + } + written += writtenRatchet + writtenRatchet, err = o.BobOneTimeKey.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmOutboundSession: %w", err) + } + written += writtenRatchet + writtenRatchet, err = o.Ratchet.PickleLibOlm(target[written:]) + if err != nil { + return 0, fmt.Errorf("pickle MegolmOutboundSession: %w", err) + } + written += writtenRatchet + return written, nil +} + +// PickleLen returns the actual number of bytes the pickled session will have. +func (o OlmSession) PickleLen() int { + length := libolmpickle.PickleUInt32Len(olmSessionPickleVersionLibOlm) + length += libolmpickle.PickleBoolLen(o.ReceivedMessage) + length += o.AliceIdentityKey.PickleLen() + length += o.AliceBaseKey.PickleLen() + length += o.BobOneTimeKey.PickleLen() + length += o.Ratchet.PickleLen() + return length +} + +// PickleLenMin returns the minimum number of bytes the pickled session must have. +func (o OlmSession) PickleLenMin() int { + length := libolmpickle.PickleUInt32Len(olmSessionPickleVersionLibOlm) + length += libolmpickle.PickleBoolLen(o.ReceivedMessage) + length += o.AliceIdentityKey.PickleLen() + length += o.AliceBaseKey.PickleLen() + length += o.BobOneTimeKey.PickleLen() + length += o.Ratchet.PickleLenMin() + return length +} + +// Describe returns a string describing the current state of the session for debugging. +func (o OlmSession) Describe() string { + var res string + if o.Ratchet.SenderChains.IsSet { + res += fmt.Sprintf("sender chain index: %d ", o.Ratchet.SenderChains.CKey.Index) + } else { + res += "sender chain index: " + } + res += "receiver chain indicies:" + for _, curChain := range o.Ratchet.ReceiverChains { + res += fmt.Sprintf(" %d", curChain.CKey.Index) + } + res += " skipped message keys:" + for _, curSkip := range o.Ratchet.SkippedMessageKeys { + res += fmt.Sprintf(" %d", curSkip.MKey.Index) + } + return res +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/utilities/main.go b/vendor/maunium.net/go/mautrix/crypto/goolm/utilities/main.go new file mode 100644 index 0000000..c5b5c2d --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/utilities/main.go @@ -0,0 +1,23 @@ +package utilities + +import ( + "encoding/base64" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/crypto" + "maunium.net/go/mautrix/id" +) + +// VerifySignature verifies an ed25519 signature. +func VerifySignature(message []byte, key id.Ed25519, signature []byte) (ok bool, err error) { + keyDecoded, err := base64.RawStdEncoding.DecodeString(string(key)) + if err != nil { + return false, err + } + signatureDecoded, err := goolm.Base64Decode(signature) + if err != nil { + return false, err + } + publicKey := crypto.Ed25519PublicKey(keyDecoded) + return publicKey.Verify(message, signatureDecoded), nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/utilities/pickle.go b/vendor/maunium.net/go/mautrix/crypto/goolm/utilities/pickle.go new file mode 100644 index 0000000..993366c --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/goolm/utilities/pickle.go @@ -0,0 +1,60 @@ +package utilities + +import ( + "encoding/json" + "fmt" + + "maunium.net/go/mautrix/crypto/goolm" + "maunium.net/go/mautrix/crypto/goolm/cipher" +) + +// PickleAsJSON returns an object as a base64 string encrypted using the supplied key. The unencrypted representation of the object is in JSON format. +func PickleAsJSON(object any, pickleVersion byte, key []byte) ([]byte, error) { + if len(key) == 0 { + return nil, fmt.Errorf("pickle: %w", goolm.ErrNoKeyProvided) + } + marshaled, err := json.Marshal(object) + if err != nil { + return nil, fmt.Errorf("pickle marshal: %w", err) + } + marshaled = append([]byte{pickleVersion}, marshaled...) + toEncrypt := make([]byte, len(marshaled)) + copy(toEncrypt, marshaled) + //pad marshaled to get block size + if len(marshaled)%cipher.PickleBlockSize() != 0 { + padding := cipher.PickleBlockSize() - len(marshaled)%cipher.PickleBlockSize() + toEncrypt = make([]byte, len(marshaled)+padding) + copy(toEncrypt, marshaled) + } + encrypted, err := cipher.Pickle(key, toEncrypt) + if err != nil { + return nil, fmt.Errorf("pickle encrypt: %w", err) + } + return encrypted, nil +} + +// UnpickleAsJSON updates the object by a base64 encrypted string using the supplied key. The unencrypted representation has to be in JSON format. +func UnpickleAsJSON(object any, pickled, key []byte, pickleVersion byte) error { + if len(key) == 0 { + return fmt.Errorf("unpickle: %w", goolm.ErrNoKeyProvided) + } + decrypted, err := cipher.Unpickle(key, pickled) + if err != nil { + return fmt.Errorf("unpickle decrypt: %w", err) + } + //unpad decrypted so unmarshal works + for i := len(decrypted) - 1; i >= 0; i-- { + if decrypted[i] != 0 { + decrypted = decrypted[:i+1] + break + } + } + if decrypted[0] != pickleVersion { + return fmt.Errorf("unpickle: %w", goolm.ErrWrongPickleVersion) + } + err = json.Unmarshal(decrypted[1:], object) + if err != nil { + return fmt.Errorf("unpickle unmarshal: %w", err) + } + return nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/keyimport.go b/vendor/maunium.net/go/mautrix/crypto/keyimport.go index ed66f23..2d9f348 100644 --- a/vendor/maunium.net/go/mautrix/crypto/keyimport.go +++ b/vendor/maunium.net/go/mautrix/crypto/keyimport.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -8,6 +8,7 @@ package crypto import ( "bytes" + "context" "crypto/aes" "crypto/cipher" "crypto/hmac" @@ -91,7 +92,7 @@ func decryptKeyExport(passphrase string, exportData []byte) ([]ExportedSession, return sessionsJSON, nil } -func (mach *OlmMachine) importExportedRoomKey(session ExportedSession) (bool, error) { +func (mach *OlmMachine) importExportedRoomKey(ctx context.Context, session ExportedSession) (bool, error) { if session.Algorithm != id.AlgorithmMegolmV1 { return false, ErrInvalidExportedAlgorithm } @@ -112,12 +113,12 @@ func (mach *OlmMachine) importExportedRoomKey(session ExportedSession) (bool, er ReceivedAt: time.Now().UTC(), } - existingIGS, _ := mach.CryptoStore.GetGroupSession(igs.RoomID, igs.SenderKey, igs.ID()) + existingIGS, _ := mach.CryptoStore.GetGroupSession(ctx, igs.RoomID, igs.SenderKey, igs.ID()) if existingIGS != nil && existingIGS.Internal.FirstKnownIndex() <= igs.Internal.FirstKnownIndex() { // We already have an equivalent or better session in the store, so don't override it. return false, nil } - err = mach.CryptoStore.PutGroupSession(igs.RoomID, igs.SenderKey, igs.ID(), igs) + err = mach.CryptoStore.PutGroupSession(ctx, igs.RoomID, igs.SenderKey, igs.ID(), igs) if err != nil { return false, fmt.Errorf("failed to store imported session: %w", err) } @@ -127,7 +128,7 @@ func (mach *OlmMachine) importExportedRoomKey(session ExportedSession) (bool, er // ImportKeys imports data that was exported with the format specified in the Matrix spec. // See https://spec.matrix.org/v1.2/client-server-api/#key-exports -func (mach *OlmMachine) ImportKeys(passphrase string, data []byte) (int, int, error) { +func (mach *OlmMachine) ImportKeys(ctx context.Context, passphrase string, data []byte) (int, int, error) { exportData, err := decodeKeyExport(data) if err != nil { return 0, 0, err @@ -143,8 +144,11 @@ func (mach *OlmMachine) ImportKeys(passphrase string, data []byte) (int, int, er Str("room_id", session.RoomID.String()). Str("session_id", session.SessionID.String()). Logger() - imported, err := mach.importExportedRoomKey(session) + imported, err := mach.importExportedRoomKey(ctx, session) if err != nil { + if ctx.Err() != nil { + return count, len(sessions), ctx.Err() + } log.Error().Err(err).Msg("Failed to import Megolm session from file") } else if imported { log.Debug().Msg("Imported Megolm session from file") diff --git a/vendor/maunium.net/go/mautrix/crypto/keysharing.go b/vendor/maunium.net/go/mautrix/crypto/keysharing.go index 1cbc41b..8cf15d3 100644 --- a/vendor/maunium.net/go/mautrix/crypto/keysharing.go +++ b/vendor/maunium.net/go/mautrix/crypto/keysharing.go @@ -1,5 +1,5 @@ // Copyright (c) 2020 Nikos Filippakis -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -48,7 +48,7 @@ func (mach *OlmMachine) RequestRoomKey(ctx context.Context, toUser id.UserID, to keyResponseReceived := make(chan struct{}) mach.roomKeyRequestFilled.Store(sessionID, keyResponseReceived) - err := mach.SendRoomKeyRequest(roomID, senderKey, sessionID, requestID, map[id.UserID][]id.DeviceID{toUser: {toDevice}}) + err := mach.SendRoomKeyRequest(ctx, roomID, senderKey, sessionID, requestID, map[id.UserID][]id.DeviceID{toUser: {toDevice}}) if err != nil { return nil, err } @@ -85,7 +85,7 @@ func (mach *OlmMachine) RequestRoomKey(ctx context.Context, toUser id.UserID, to }, } - mach.Client.SendToDevice(event.ToDeviceRoomKeyRequest, toDeviceCancel) + mach.Client.SendToDevice(ctx, event.ToDeviceRoomKeyRequest, toDeviceCancel) }() return resChan, nil } @@ -99,7 +99,7 @@ func (mach *OlmMachine) RequestRoomKey(ctx context.Context, toUser id.UserID, to // to the specific key request, but currently it only supports a single target device and is therefore deprecated. // A future function may properly support multiple targets and automatically canceling the other requests when receiving // the first response. -func (mach *OlmMachine) SendRoomKeyRequest(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, requestID string, users map[id.UserID][]id.DeviceID) error { +func (mach *OlmMachine) SendRoomKeyRequest(ctx context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, requestID string, users map[id.UserID][]id.DeviceID) error { if len(requestID) == 0 { requestID = mach.Client.TxnID() } @@ -126,7 +126,7 @@ func (mach *OlmMachine) SendRoomKeyRequest(roomID id.RoomID, senderKey id.Sender toDeviceReq.Messages[user][device] = requestEvent } } - _, err := mach.Client.SendToDevice(event.ToDeviceRoomKeyRequest, toDeviceReq) + _, err := mach.Client.SendToDevice(ctx, event.ToDeviceRoomKeyRequest, toDeviceReq) return err } @@ -152,7 +152,10 @@ func (mach *OlmMachine) importForwardedRoomKey(ctx context.Context, evt *Decrypt Msg("Mismatched session ID while creating inbound group session from forward") return false } - config := mach.StateStore.GetEncryptionEvent(content.RoomID) + config, err := mach.StateStore.GetEncryptionEvent(ctx, content.RoomID) + if err != nil { + log.Error().Err(err).Msg("Failed to get encryption event for room") + } var maxAge time.Duration var maxMessages int if config != nil { @@ -178,7 +181,7 @@ func (mach *OlmMachine) importForwardedRoomKey(ctx context.Context, evt *Decrypt MaxMessages: maxMessages, IsScheduled: content.IsScheduled, } - err = mach.CryptoStore.PutGroupSession(content.RoomID, content.SenderKey, content.SessionID, igs) + err = mach.CryptoStore.PutGroupSession(ctx, content.RoomID, content.SenderKey, content.SessionID, igs) if err != nil { log.Error().Err(err).Msg("Failed to store new inbound group session") return false @@ -188,7 +191,7 @@ func (mach *OlmMachine) importForwardedRoomKey(ctx context.Context, evt *Decrypt return true } -func (mach *OlmMachine) rejectKeyRequest(rejection KeyShareRejection, device *id.Device, request event.RequestedKeyInfo) { +func (mach *OlmMachine) rejectKeyRequest(ctx context.Context, rejection KeyShareRejection, device *id.Device, request event.RequestedKeyInfo) { if rejection.Code == "" { // If the rejection code is empty, it means don't share keys, but also don't tell the requester. return @@ -201,7 +204,7 @@ func (mach *OlmMachine) rejectKeyRequest(rejection KeyShareRejection, device *id Code: rejection.Code, Reason: rejection.Reason, } - err := mach.sendToOneDevice(device.UserID, device.DeviceID, event.ToDeviceRoomKeyWithheld, &content) + err := mach.sendToOneDevice(ctx, device.UserID, device.DeviceID, event.ToDeviceRoomKeyWithheld, &content) if err != nil { mach.Log.Warn().Err(err). Str("code", string(rejection.Code)). @@ -209,7 +212,7 @@ func (mach *OlmMachine) rejectKeyRequest(rejection KeyShareRejection, device *id Str("device_id", device.DeviceID.String()). Msg("Failed to send key share rejection") } - err = mach.sendToOneDevice(device.UserID, device.DeviceID, event.ToDeviceOrgMatrixRoomKeyWithheld, &content) + err = mach.sendToOneDevice(ctx, device.UserID, device.DeviceID, event.ToDeviceOrgMatrixRoomKeyWithheld, &content) if err != nil { mach.Log.Warn().Err(err). Str("code", string(rejection.Code)). @@ -270,23 +273,23 @@ func (mach *OlmMachine) handleRoomKeyRequest(ctx context.Context, sender id.User rejection := mach.AllowKeyShare(ctx, device, content.Body) if rejection != nil { - mach.rejectKeyRequest(*rejection, device, content.Body) + mach.rejectKeyRequest(ctx, *rejection, device, content.Body) return } - igs, err := mach.CryptoStore.GetGroupSession(content.Body.RoomID, content.Body.SenderKey, content.Body.SessionID) + igs, err := mach.CryptoStore.GetGroupSession(ctx, content.Body.RoomID, content.Body.SenderKey, content.Body.SessionID) if err != nil { if errors.Is(err, ErrGroupSessionWithheld) { log.Debug().Err(err).Msg("Requested group session not available") - mach.rejectKeyRequest(KeyShareRejectUnavailable, device, content.Body) + mach.rejectKeyRequest(ctx, KeyShareRejectUnavailable, device, content.Body) } else { log.Error().Err(err).Msg("Failed to get group session to forward") - mach.rejectKeyRequest(KeyShareRejectInternalError, device, content.Body) + mach.rejectKeyRequest(ctx, KeyShareRejectInternalError, device, content.Body) } return } else if igs == nil { log.Error().Msg("Didn't find group session to forward") - mach.rejectKeyRequest(KeyShareRejectUnavailable, device, content.Body) + mach.rejectKeyRequest(ctx, KeyShareRejectUnavailable, device, content.Body) return } if internalID := igs.ID(); internalID != content.Body.SessionID { @@ -299,7 +302,7 @@ func (mach *OlmMachine) handleRoomKeyRequest(ctx context.Context, sender id.User exportedKey, err := igs.Internal.Export(firstKnownIndex) if err != nil { log.Error().Err(err).Msg("Failed to export group session to forward") - mach.rejectKeyRequest(KeyShareRejectInternalError, device, content.Body) + mach.rejectKeyRequest(ctx, KeyShareRejectInternalError, device, content.Body) return } @@ -331,7 +334,7 @@ func (mach *OlmMachine) handleBeeperRoomKeyAck(ctx context.Context, sender id.Us Int("first_message_index", content.FirstMessageIndex). Logger() - sess, err := mach.CryptoStore.GetGroupSession(content.RoomID, "", content.SessionID) + sess, err := mach.CryptoStore.GetGroupSession(ctx, content.RoomID, "", content.SessionID) if err != nil { if errors.Is(err, ErrGroupSessionWithheld) { log.Debug().Err(err).Msg("Acked group session was already redacted") @@ -351,7 +354,7 @@ func (mach *OlmMachine) handleBeeperRoomKeyAck(ctx context.Context, sender id.Us isInbound := sess.SenderKey == mach.OwnIdentity().IdentityKey if isInbound && mach.DeleteOutboundKeysOnAck && content.FirstMessageIndex == 0 { log.Debug().Msg("Redacting inbound copy of outbound group session after ack") - err = mach.CryptoStore.RedactGroupSession(content.RoomID, sess.SenderKey, content.SessionID, "outbound session acked") + err = mach.CryptoStore.RedactGroupSession(ctx, content.RoomID, sess.SenderKey, content.SessionID, "outbound session acked") if err != nil { log.Err(err).Msg("Failed to redact group session") } diff --git a/vendor/maunium.net/go/mautrix/crypto/machine.go b/vendor/maunium.net/go/mautrix/crypto/machine.go index 2c9b63c..b7c41ab 100644 --- a/vendor/maunium.net/go/mautrix/crypto/machine.go +++ b/vendor/maunium.net/go/mautrix/crypto/machine.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -33,6 +33,9 @@ type OlmMachine struct { PlaintextMentions bool + // Never ask the server for keys automatically as a side effect. + DisableKeyFetching bool + SendKeysMinTrust id.TrustState ShareKeysMinTrust id.TrustState @@ -80,11 +83,11 @@ type OlmMachine struct { // StateStore is used by OlmMachine to get room state information that's needed for encryption. type StateStore interface { // IsEncrypted returns whether a room is encrypted. - IsEncrypted(id.RoomID) bool + IsEncrypted(context.Context, id.RoomID) (bool, error) // GetEncryptionEvent returns the encryption event's content for an encrypted room. - GetEncryptionEvent(id.RoomID) *event.EncryptionEventContent + GetEncryptionEvent(context.Context, id.RoomID) (*event.EncryptionEventContent, error) // FindSharedRooms returns the encrypted rooms that another user is also in for a user ID. - FindSharedRooms(id.UserID) []id.RoomID + FindSharedRooms(context.Context, id.UserID) ([]id.RoomID, error) } // NewOlmMachine creates an OlmMachine with the given client, logger and stores. @@ -131,8 +134,8 @@ func (mach *OlmMachine) machOrContextLog(ctx context.Context) *zerolog.Logger { // Load loads the Olm account information from the crypto store. If there's no olm account, a new one is created. // This must be called before using the machine. -func (mach *OlmMachine) Load() (err error) { - mach.account, err = mach.CryptoStore.GetAccount() +func (mach *OlmMachine) Load(ctx context.Context) (err error) { + mach.account, err = mach.CryptoStore.GetAccount(ctx) if err != nil { return } @@ -142,16 +145,16 @@ func (mach *OlmMachine) Load() (err error) { return nil } -func (mach *OlmMachine) saveAccount() { - err := mach.CryptoStore.PutAccount(mach.account) +func (mach *OlmMachine) saveAccount(ctx context.Context) { + err := mach.CryptoStore.PutAccount(ctx, mach.account) if err != nil { mach.Log.Error().Err(err).Msg("Failed to save account") } } // FlushStore calls the Flush method of the CryptoStore. -func (mach *OlmMachine) FlushStore() error { - return mach.CryptoStore.Flush() +func (mach *OlmMachine) FlushStore(ctx context.Context) error { + return mach.CryptoStore.Flush(ctx) } func (mach *OlmMachine) timeTrace(ctx context.Context, thing string, expectedDuration time.Duration) func() { @@ -194,9 +197,9 @@ func (mach *OlmMachine) OwnIdentity() *id.Device { } type asEventProcessor interface { - On(evtType event.Type, handler func(evt *event.Event)) - OnOTK(func(otk *mautrix.OTKCount)) - OnDeviceList(func(lists *mautrix.DeviceLists, since string)) + On(evtType event.Type, handler func(ctx context.Context, evt *event.Event)) + OnOTK(func(ctx context.Context, otk *mautrix.OTKCount)) + OnDeviceList(func(ctx context.Context, lists *mautrix.DeviceLists, since string)) } func (mach *OlmMachine) AddAppserviceListener(ep asEventProcessor) { @@ -217,19 +220,23 @@ func (mach *OlmMachine) AddAppserviceListener(ep asEventProcessor) { mach.Log.Debug().Msg("Added listeners for encryption data coming from appservice transactions") } -func (mach *OlmMachine) HandleDeviceLists(dl *mautrix.DeviceLists, since string) { +func (mach *OlmMachine) HandleDeviceLists(ctx context.Context, dl *mautrix.DeviceLists, since string) { if len(dl.Changed) > 0 { traceID := time.Now().Format("15:04:05.000000") mach.Log.Debug(). Str("trace_id", traceID). Interface("changes", dl.Changed). Msg("Device list changes in /sync") - mach.fetchKeys(context.TODO(), dl.Changed, since, false) + if mach.DisableKeyFetching { + mach.CryptoStore.MarkTrackedUsersOutdated(ctx, dl.Changed) + } else { + mach.FetchKeys(ctx, dl.Changed, false) + } mach.Log.Debug().Str("trace_id", traceID).Msg("Finished handling device list changes") } } -func (mach *OlmMachine) HandleOTKCounts(otkCount *mautrix.OTKCount) { +func (mach *OlmMachine) HandleOTKCounts(ctx context.Context, otkCount *mautrix.OTKCount) { if (len(otkCount.UserID) > 0 && otkCount.UserID != mach.Client.UserID) || (len(otkCount.DeviceID) > 0 && otkCount.DeviceID != mach.Client.DeviceID) { // TODO This log probably needs to be silence-able if someone wants to use encrypted appservices with multiple e2ee sessions mach.Log.Warn(). @@ -243,7 +250,7 @@ func (mach *OlmMachine) HandleOTKCounts(otkCount *mautrix.OTKCount) { if otkCount.SignedCurve25519 < int(minCount) { traceID := time.Now().Format("15:04:05.000000") log := mach.Log.With().Str("trace_id", traceID).Logger() - ctx := log.WithContext(context.Background()) + ctx = log.WithContext(ctx) log.Debug(). Int("keys_left", otkCount.Curve25519). Msg("Sync response said we have less than 50 signed curve25519 keys left, sharing new ones...") @@ -261,8 +268,8 @@ func (mach *OlmMachine) HandleOTKCounts(otkCount *mautrix.OTKCount) { // This can be easily registered into a mautrix client using .OnSync(): // // client.Syncer.(mautrix.ExtensibleSyncer).OnSync(c.crypto.ProcessSyncResponse) -func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync, since string) bool { - mach.HandleDeviceLists(&resp.DeviceLists, since) +func (mach *OlmMachine) ProcessSyncResponse(ctx context.Context, resp *mautrix.RespSync, since string) bool { + mach.HandleDeviceLists(ctx, &resp.DeviceLists, since) for _, evt := range resp.ToDevice.Events { evt.Type.Class = event.ToDeviceEventType @@ -271,10 +278,10 @@ func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync, since string mach.Log.Warn().Str("event_type", evt.Type.Type).Err(err).Msg("Failed to parse to-device event") continue } - mach.HandleToDeviceEvent(evt) + mach.HandleToDeviceEvent(ctx, evt) } - mach.HandleOTKCounts(&resp.DeviceOTKCount) + mach.HandleOTKCounts(ctx, &resp.DeviceOTKCount) return true } @@ -283,8 +290,12 @@ func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync, since string // Currently this is not automatically called, so you must add a listener yourself: // // client.Syncer.(mautrix.ExtensibleSyncer).OnEventType(event.StateMember, c.crypto.HandleMemberEvent) -func (mach *OlmMachine) HandleMemberEvent(_ mautrix.EventSource, evt *event.Event) { - if !mach.StateStore.IsEncrypted(evt.RoomID) { +func (mach *OlmMachine) HandleMemberEvent(ctx context.Context, evt *event.Event) { + if isEncrypted, err := mach.StateStore.IsEncrypted(ctx, evt.RoomID); err != nil { + mach.machOrContextLog(ctx).Err(err).Stringer("room_id", evt.RoomID). + Msg("Failed to check if room is encrypted to handle member event") + return + } else if !isEncrypted { return } content := evt.Content.AsMember() @@ -311,7 +322,7 @@ func (mach *OlmMachine) HandleMemberEvent(_ mautrix.EventSource, evt *event.Even Str("prev_membership", string(prevContent.Membership)). Str("new_membership", string(content.Membership)). Msg("Got membership state change, invalidating group session in room") - err := mach.CryptoStore.RemoveOutboundGroupSession(evt.RoomID) + err := mach.CryptoStore.RemoveOutboundGroupSession(ctx, evt.RoomID) if err != nil { mach.Log.Warn().Str("room_id", evt.RoomID.String()).Msg("Failed to invalidate outbound group session") } @@ -319,7 +330,7 @@ func (mach *OlmMachine) HandleMemberEvent(_ mautrix.EventSource, evt *event.Even // HandleToDeviceEvent handles a single to-device event. This is automatically called by ProcessSyncResponse, so you // don't need to add any custom handlers if you use that method. -func (mach *OlmMachine) HandleToDeviceEvent(evt *event.Event) { +func (mach *OlmMachine) HandleToDeviceEvent(ctx context.Context, evt *event.Event) { if len(evt.ToUserID) > 0 && (evt.ToUserID != mach.Client.UserID || evt.ToDeviceID != mach.Client.DeviceID) { // TODO This log probably needs to be silence-able if someone wants to use encrypted appservices with multiple e2ee sessions mach.Log.Debug(). @@ -329,12 +340,13 @@ func (mach *OlmMachine) HandleToDeviceEvent(evt *event.Event) { return } traceID := time.Now().Format("15:04:05.000000") + // TODO use context log? log := mach.Log.With(). Str("trace_id", traceID). Str("sender", evt.Sender.String()). Str("type", evt.Type.Type). Logger() - ctx := log.WithContext(context.Background()) + ctx = log.WithContext(ctx) if evt.Type != event.ToDeviceEncrypted { log.Debug().Msg("Starting handling to-device event") } @@ -344,7 +356,7 @@ func (mach *OlmMachine) HandleToDeviceEvent(evt *event.Event) { Str("sender_key", content.SenderKey.String()). Logger() log.Debug().Msg("Handling encrypted to-device event") - ctx = log.WithContext(context.Background()) + ctx = log.WithContext(ctx) decryptedEvt, err := mach.decryptOlmEvent(ctx, evt) if err != nil { log.Error().Err(err).Msg("Failed to decrypt to-device event") @@ -381,17 +393,17 @@ func (mach *OlmMachine) HandleToDeviceEvent(evt *event.Event) { mach.handleBeeperRoomKeyAck(ctx, evt.Sender, content) // verification cases case *event.VerificationStartEventContent: - mach.handleVerificationStart(evt.Sender, content, content.TransactionID, 10*time.Minute, "") + mach.handleVerificationStart(ctx, evt.Sender, content, content.TransactionID, 10*time.Minute, "") case *event.VerificationAcceptEventContent: - mach.handleVerificationAccept(evt.Sender, content, content.TransactionID) + mach.handleVerificationAccept(ctx, evt.Sender, content, content.TransactionID) case *event.VerificationKeyEventContent: - mach.handleVerificationKey(evt.Sender, content, content.TransactionID) + mach.handleVerificationKey(ctx, evt.Sender, content, content.TransactionID) case *event.VerificationMacEventContent: - mach.handleVerificationMAC(evt.Sender, content, content.TransactionID) + mach.handleVerificationMAC(ctx, evt.Sender, content, content.TransactionID) case *event.VerificationCancelEventContent: mach.handleVerificationCancel(evt.Sender, content, content.TransactionID) case *event.VerificationRequestEventContent: - mach.handleVerificationRequest(evt.Sender, content, content.TransactionID, "") + mach.handleVerificationRequest(ctx, evt.Sender, content, content.TransactionID, "") case *event.RoomKeyWithheldEventContent: mach.handleRoomKeyWithheld(ctx, content) default: @@ -405,14 +417,15 @@ func (mach *OlmMachine) HandleToDeviceEvent(evt *event.Event) { // GetOrFetchDevice attempts to retrieve the device identity for the given device from the store // and if it's not found it asks the server for it. func (mach *OlmMachine) GetOrFetchDevice(ctx context.Context, userID id.UserID, deviceID id.DeviceID) (*id.Device, error) { - device, err := mach.CryptoStore.GetDevice(userID, deviceID) + device, err := mach.CryptoStore.GetDevice(ctx, userID, deviceID) if err != nil { return nil, fmt.Errorf("failed to get sender device from store: %w", err) - } else if device != nil { + } else if device != nil || mach.DisableKeyFetching { return device, nil } - usersToDevices := mach.fetchKeys(ctx, []id.UserID{userID}, "", true) - if devices, ok := usersToDevices[userID]; ok { + if usersToDevices, err := mach.FetchKeys(ctx, []id.UserID{userID}, true); err != nil { + return nil, fmt.Errorf("failed to fetch keys: %w", err) + } else if devices, ok := usersToDevices[userID]; ok { if device, ok = devices[deviceID]; ok { return device, nil } @@ -425,15 +438,15 @@ func (mach *OlmMachine) GetOrFetchDevice(ctx context.Context, userID id.UserID, // store and if it's not found it asks the server for it. This returns nil if the server doesn't return a device with // the given identity key. func (mach *OlmMachine) GetOrFetchDeviceByKey(ctx context.Context, userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) { - deviceIdentity, err := mach.CryptoStore.FindDeviceByKey(userID, identityKey) - if err != nil || deviceIdentity != nil { + deviceIdentity, err := mach.CryptoStore.FindDeviceByKey(ctx, userID, identityKey) + if err != nil || deviceIdentity != nil || mach.DisableKeyFetching { return deviceIdentity, err } mach.machOrContextLog(ctx).Debug(). Str("user_id", userID.String()). Str("identity_key", identityKey.String()). Msg("Didn't find identity in crypto store, fetching from server") - devices := mach.LoadDevices(userID) + devices := mach.LoadDevices(ctx, userID) for _, device := range devices { if device.IdentityKey == identityKey { return device, nil @@ -455,7 +468,7 @@ func (mach *OlmMachine) SendEncryptedToDevice(ctx context.Context, device *id.De mach.olmLock.Lock() defer mach.olmLock.Unlock() - olmSess, err := mach.CryptoStore.GetLatestSession(device.IdentityKey) + olmSess, err := mach.CryptoStore.GetLatestSession(ctx, device.IdentityKey) if err != nil { return err } @@ -473,7 +486,7 @@ func (mach *OlmMachine) SendEncryptedToDevice(ctx context.Context, device *id.De Str("to_identity_key", device.IdentityKey.String()). Str("olm_session_id", olmSess.ID().String()). Msg("Sending encrypted to-device event") - _, err = mach.Client.SendToDevice(event.ToDeviceEncrypted, + _, err = mach.Client.SendToDevice(ctx, event.ToDeviceEncrypted, &mautrix.ReqSendToDevice{ Messages: map[id.UserID]map[id.DeviceID]*event.Content{ device.UserID: { @@ -499,7 +512,7 @@ func (mach *OlmMachine) createGroupSession(ctx context.Context, senderKey id.Sen Msg("Mismatched session ID while creating inbound group session") return } - err = mach.CryptoStore.PutGroupSession(roomID, senderKey, sessionID, igs) + err = mach.CryptoStore.PutGroupSession(ctx, roomID, senderKey, sessionID, igs) if err != nil { log.Error().Err(err).Str("session_id", sessionID.String()).Msg("Failed to store new inbound group session") return @@ -525,7 +538,7 @@ func (mach *OlmMachine) markSessionReceived(id id.SessionID) { } // WaitForSession waits for the given Megolm session to arrive. -func (mach *OlmMachine) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool { +func (mach *OlmMachine) WaitForSession(ctx context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool { mach.keyWaitersLock.Lock() ch, ok := mach.keyWaiters[sessionID] if !ok { @@ -534,7 +547,7 @@ func (mach *OlmMachine) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, } mach.keyWaitersLock.Unlock() // Handle race conditions where a session appears between the failed decryption and WaitForSession call. - sess, err := mach.CryptoStore.GetGroupSession(roomID, senderKey, sessionID) + sess, err := mach.CryptoStore.GetGroupSession(ctx, roomID, senderKey, sessionID) if sess != nil || errors.Is(err, ErrGroupSessionWithheld) { return true } @@ -542,10 +555,12 @@ func (mach *OlmMachine) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, case <-ch: return true case <-time.After(timeout): - sess, err = mach.CryptoStore.GetGroupSession(roomID, senderKey, sessionID) + sess, err = mach.CryptoStore.GetGroupSession(ctx, roomID, senderKey, sessionID) // Check if the session somehow appeared in the store without telling us // We accept withheld sessions as received, as then the decryption attempt will show the error. return sess != nil || errors.Is(err, ErrGroupSessionWithheld) + case <-ctx.Done(): + return false } } @@ -568,7 +583,10 @@ func (mach *OlmMachine) receiveRoomKey(ctx context.Context, evt *DecryptedOlmEve return } - config := mach.StateStore.GetEncryptionEvent(content.RoomID) + config, err := mach.StateStore.GetEncryptionEvent(ctx, content.RoomID) + if err != nil { + log.Error().Err(err).Msg("Failed to get encryption event for room") + } var maxAge time.Duration var maxMessages int if config != nil { @@ -589,7 +607,7 @@ func (mach *OlmMachine) receiveRoomKey(ctx context.Context, evt *DecryptedOlmEve } if mach.DeletePreviousKeysOnReceive && !content.IsScheduled { log.Debug().Msg("Redacting previous megolm sessions from sender in room") - sessionIDs, err := mach.CryptoStore.RedactGroupSessions(content.RoomID, evt.SenderKey, "received new key from device") + sessionIDs, err := mach.CryptoStore.RedactGroupSessions(ctx, content.RoomID, evt.SenderKey, "received new key from device") if err != nil { log.Err(err).Msg("Failed to redact previous megolm sessions") } else { @@ -606,7 +624,7 @@ func (mach *OlmMachine) handleRoomKeyWithheld(ctx context.Context, content *even zerolog.Ctx(ctx).Debug().Interface("content", content).Msg("Non-megolm room key withheld event") return } - err := mach.CryptoStore.PutWithheldGroupSession(*content) + err := mach.CryptoStore.PutWithheldGroupSession(ctx, *content) if err != nil { zerolog.Ctx(ctx).Error().Err(err).Msg("Failed to save room key withheld event") } @@ -624,7 +642,7 @@ func (mach *OlmMachine) ShareKeys(ctx context.Context, currentOTKCount int) erro defer mach.otkUploadLock.Unlock() if mach.lastOTKUpload.Add(1*time.Minute).After(start) || currentOTKCount < 0 { log.Debug().Msg("Checking OTK count from server due to suspiciously close share keys requests or negative OTK count") - resp, err := mach.Client.UploadKeys(&mautrix.ReqUploadKeys{}) + resp, err := mach.Client.UploadKeys(ctx, &mautrix.ReqUploadKeys{}) if err != nil { return fmt.Errorf("failed to check current OTK counts: %w", err) } @@ -637,6 +655,15 @@ func (mach *OlmMachine) ShareKeys(ctx context.Context, currentOTKCount int) erro var deviceKeys *mautrix.DeviceKeys if !mach.account.Shared { deviceKeys = mach.account.getInitialKeys(mach.Client.UserID, mach.Client.DeviceID) + err := mach.CryptoStore.PutDevice(ctx, mach.Client.UserID, &id.Device{ + UserID: mach.Client.UserID, + DeviceID: mach.Client.DeviceID, + IdentityKey: deviceKeys.Keys.GetCurve25519(mach.Client.DeviceID), + SigningKey: deviceKeys.Keys.GetEd25519(mach.Client.DeviceID), + }) + if err != nil { + return fmt.Errorf("failed to save initial keys: %w", err) + } log.Debug().Msg("Going to upload initial account keys") } oneTimeKeys := mach.account.getOneTimeKeys(mach.Client.UserID, mach.Client.DeviceID, currentOTKCount) @@ -649,20 +676,20 @@ func (mach *OlmMachine) ShareKeys(ctx context.Context, currentOTKCount int) erro OneTimeKeys: oneTimeKeys, } log.Debug().Int("count", len(oneTimeKeys)).Msg("Uploading one-time keys") - _, err := mach.Client.UploadKeys(req) + _, err := mach.Client.UploadKeys(ctx, req) if err != nil { return err } mach.lastOTKUpload = time.Now() mach.account.Shared = true - mach.saveAccount() + mach.saveAccount(ctx) return nil } func (mach *OlmMachine) ExpiredKeyDeleteLoop(ctx context.Context) { log := mach.Log.With().Str("action", "redact expired sessions").Logger() for { - sessionIDs, err := mach.CryptoStore.RedactExpiredGroupSessions() + sessionIDs, err := mach.CryptoStore.RedactExpiredGroupSessions(ctx) if err != nil { log.Err(err).Msg("Failed to redact expired megolm sessions") } else if len(sessionIDs) > 0 { diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/LICENSE b/vendor/maunium.net/go/mautrix/crypto/olm/LICENSE deleted file mode 100644 index f433b1a..0000000 --- a/vendor/maunium.net/go/mautrix/crypto/olm/LICENSE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/README.md b/vendor/maunium.net/go/mautrix/crypto/olm/README.md index 1b27f63..7d8086c 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/README.md +++ b/vendor/maunium.net/go/mautrix/crypto/olm/README.md @@ -1,2 +1,4 @@ # Go olm bindings Based on [Dhole/go-olm](https://github.com/Dhole/go-olm) + +The original project is licensed under the Apache 2.0 license. diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/account.go b/vendor/maunium.net/go/mautrix/crypto/olm/account.go index c3d8026..37458d1 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/account.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/account.go @@ -1,3 +1,5 @@ +//go:build !goolm + package olm // #cgo LDFLAGS: -lolm -lstdc++ @@ -155,6 +157,7 @@ func (a *Account) Unpickle(pickled, key []byte) error { return nil } +// Deprecated func (a *Account) GobEncode() ([]byte, error) { pickled := a.Pickle(pickleKey) length := base64.RawStdEncoding.DecodedLen(len(pickled)) @@ -163,6 +166,7 @@ func (a *Account) GobEncode() ([]byte, error) { return rawPickled, err } +// Deprecated func (a *Account) GobDecode(rawPickled []byte) error { if a.int == nil { *a = *NewBlankAccount() @@ -173,6 +177,7 @@ func (a *Account) GobDecode(rawPickled []byte) error { return a.Unpickle(pickled, pickleKey) } +// Deprecated func (a *Account) MarshalJSON() ([]byte, error) { pickled := a.Pickle(pickleKey) quotes := make([]byte, len(pickled)+2) @@ -182,6 +187,7 @@ func (a *Account) MarshalJSON() ([]byte, error) { return quotes, nil } +// Deprecated func (a *Account) UnmarshalJSON(data []byte) error { if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' { return InputNotJSONString diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/account_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/account_goolm.go new file mode 100644 index 0000000..eeff54f --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/account_goolm.go @@ -0,0 +1,154 @@ +//go:build goolm + +package olm + +import ( + "encoding/json" + + "github.com/tidwall/sjson" + + "maunium.net/go/mautrix/crypto/canonicaljson" + "maunium.net/go/mautrix/crypto/goolm/account" + "maunium.net/go/mautrix/id" +) + +// Account stores a device account for end to end encrypted messaging. +type Account struct { + account.Account +} + +// NewAccount creates a new Account. +func NewAccount() *Account { + a, err := account.NewAccount(nil) + if err != nil { + panic(err) + } + ac := &Account{} + ac.Account = *a + return ac +} + +func NewBlankAccount() *Account { + return &Account{} +} + +// Clear clears the memory used to back this Account. +func (a *Account) Clear() error { + a.Account = account.Account{} + return nil +} + +// Pickle returns an Account as a base64 string. Encrypts the Account using the +// supplied key. +func (a *Account) Pickle(key []byte) []byte { + if len(key) == 0 { + panic(NoKeyProvided) + } + pickled, err := a.Account.Pickle(key) + if err != nil { + panic(err) + } + return pickled +} + +// IdentityKeysJSON returns the public parts of the identity keys for the Account. +func (a *Account) IdentityKeysJSON() []byte { + identityKeys, err := a.Account.IdentityKeysJSON() + if err != nil { + panic(err) + } + return identityKeys +} + +// Sign returns the signature of a message using the ed25519 key for this +// Account. +func (a *Account) Sign(message []byte) []byte { + if len(message) == 0 { + panic(EmptyInput) + } + signature, err := a.Account.Sign(message) + if err != nil { + panic(err) + } + return signature +} + +// SignJSON signs the given JSON object following the Matrix specification: +// https://matrix.org/docs/spec/appendices#signing-json +func (a *Account) SignJSON(obj interface{}) (string, error) { + objJSON, err := json.Marshal(obj) + if err != nil { + return "", err + } + objJSON, _ = sjson.DeleteBytes(objJSON, "unsigned") + objJSON, _ = sjson.DeleteBytes(objJSON, "signatures") + return string(a.Sign(canonicaljson.CanonicalJSONAssumeValid(objJSON))), nil +} + +// MaxNumberOfOneTimeKeys returns the largest number of one time keys this +// Account can store. +func (a *Account) MaxNumberOfOneTimeKeys() uint { + return uint(account.MaxOneTimeKeys) +} + +// GenOneTimeKeys generates a number of new one time keys. If the total number +// of keys stored by this Account exceeds MaxNumberOfOneTimeKeys then the old +// keys are discarded. +func (a *Account) GenOneTimeKeys(num uint) { + err := a.Account.GenOneTimeKeys(nil, num) + if err != nil { + panic(err) + } +} + +// NewOutboundSession creates a new out-bound session for sending messages to a +// given curve25519 identityKey and oneTimeKey. Returns error on failure. +func (a *Account) NewOutboundSession(theirIdentityKey, theirOneTimeKey id.Curve25519) (*Session, error) { + if len(theirIdentityKey) == 0 || len(theirOneTimeKey) == 0 { + return nil, EmptyInput + } + s := &Session{} + newSession, err := a.Account.NewOutboundSession(theirIdentityKey, theirOneTimeKey) + if err != nil { + return nil, err + } + s.OlmSession = *newSession + return s, nil +} + +// NewInboundSession creates a new in-bound session for sending/receiving +// messages from an incoming PRE_KEY message. Returns error on failure. +func (a *Account) NewInboundSession(oneTimeKeyMsg string) (*Session, error) { + if len(oneTimeKeyMsg) == 0 { + return nil, EmptyInput + } + s := &Session{} + newSession, err := a.Account.NewInboundSession(nil, []byte(oneTimeKeyMsg)) + if err != nil { + return nil, err + } + s.OlmSession = *newSession + return s, nil +} + +// NewInboundSessionFrom creates a new in-bound session for sending/receiving +// messages from an incoming PRE_KEY message. Returns error on failure. +func (a *Account) NewInboundSessionFrom(theirIdentityKey id.Curve25519, oneTimeKeyMsg string) (*Session, error) { + if len(theirIdentityKey) == 0 || len(oneTimeKeyMsg) == 0 { + return nil, EmptyInput + } + s := &Session{} + newSession, err := a.Account.NewInboundSession(&theirIdentityKey, []byte(oneTimeKeyMsg)) + if err != nil { + return nil, err + } + s.OlmSession = *newSession + return s, nil +} + +// RemoveOneTimeKeys removes the one time keys that the session used from the +// Account. Returns error on failure. +func (a *Account) RemoveOneTimeKeys(s *Session) error { + a.Account.RemoveOneTimeKeys(&s.OlmSession) + return nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/error.go b/vendor/maunium.net/go/mautrix/crypto/olm/error.go index 70a32c7..63352e2 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/error.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/error.go @@ -1,3 +1,5 @@ +//go:build !goolm + package olm import ( diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/error_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/error_goolm.go new file mode 100644 index 0000000..0e54e56 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/error_goolm.go @@ -0,0 +1,23 @@ +//go:build goolm + +package olm + +import ( + "errors" + + "maunium.net/go/mautrix/crypto/goolm" +) + +// Error codes from go-olm +var ( + EmptyInput = goolm.ErrEmptyInput + NoKeyProvided = goolm.ErrNoKeyProvided + NotEnoughGoRandom = errors.New("couldn't get enough randomness from crypto/rand") + SignatureNotFound = errors.New("input JSON doesn't contain signature from specified device") + InputNotJSONString = errors.New("input doesn't look like a JSON string") +) + +// Error codes from olm code +var ( + UnknownMessageIndex = goolm.ErrRatchetNotAvailable +) diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go b/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go index 93e54ff..a3bd3b6 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go @@ -1,3 +1,5 @@ +//go:build !goolm + package olm // #cgo LDFLAGS: -lolm -lstdc++ @@ -147,6 +149,7 @@ func (s *InboundGroupSession) Unpickle(pickled, key []byte) error { return nil } +// Deprecated func (s *InboundGroupSession) GobEncode() ([]byte, error) { pickled := s.Pickle(pickleKey) length := base64.RawStdEncoding.DecodedLen(len(pickled)) @@ -155,6 +158,7 @@ func (s *InboundGroupSession) GobEncode() ([]byte, error) { return rawPickled, err } +// Deprecated func (s *InboundGroupSession) GobDecode(rawPickled []byte) error { if s == nil || s.int == nil { *s = *NewBlankInboundGroupSession() @@ -165,6 +169,7 @@ func (s *InboundGroupSession) GobDecode(rawPickled []byte) error { return s.Unpickle(pickled, pickleKey) } +// Deprecated func (s *InboundGroupSession) MarshalJSON() ([]byte, error) { pickled := s.Pickle(pickleKey) quotes := make([]byte, len(pickled)+2) @@ -174,6 +179,7 @@ func (s *InboundGroupSession) MarshalJSON() ([]byte, error) { return quotes, nil } +// Deprecated func (s *InboundGroupSession) UnmarshalJSON(data []byte) error { if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' { return InputNotJSONString diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession_goolm.go new file mode 100644 index 0000000..4e561cf --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession_goolm.go @@ -0,0 +1,149 @@ +//go:build goolm + +package olm + +import ( + "maunium.net/go/mautrix/crypto/goolm/session" + "maunium.net/go/mautrix/id" +) + +// InboundGroupSession stores an inbound encrypted messaging session for a +// group. +type InboundGroupSession struct { + session.MegolmInboundSession +} + +// InboundGroupSessionFromPickled loads an InboundGroupSession from a pickled +// base64 string. Decrypts the InboundGroupSession using the supplied key. +// Returns error on failure. +func InboundGroupSessionFromPickled(pickled, key []byte) (*InboundGroupSession, error) { + if len(pickled) == 0 { + return nil, EmptyInput + } + lenKey := len(key) + if lenKey == 0 { + key = []byte(" ") + } + megolmSession, err := session.MegolmInboundSessionFromPickled(pickled, key) + if err != nil { + return nil, err + } + return &InboundGroupSession{ + MegolmInboundSession: *megolmSession, + }, nil +} + +// NewInboundGroupSession creates a new inbound group session from a key +// exported from OutboundGroupSession.Key(). Returns error on failure. +func NewInboundGroupSession(sessionKey []byte) (*InboundGroupSession, error) { + if len(sessionKey) == 0 { + return nil, EmptyInput + } + megolmSession, err := session.NewMegolmInboundSession(sessionKey) + if err != nil { + return nil, err + } + return &InboundGroupSession{ + MegolmInboundSession: *megolmSession, + }, nil +} + +// InboundGroupSessionImport imports an inbound group session from a previous +// export. Returns error on failure. +func InboundGroupSessionImport(sessionKey []byte) (*InboundGroupSession, error) { + if len(sessionKey) == 0 { + return nil, EmptyInput + } + megolmSession, err := session.NewMegolmInboundSessionFromExport(sessionKey) + if err != nil { + return nil, err + } + return &InboundGroupSession{ + MegolmInboundSession: *megolmSession, + }, nil +} + +func NewBlankInboundGroupSession() *InboundGroupSession { + return &InboundGroupSession{} +} + +// Clear clears the memory used to back this InboundGroupSession. +func (s *InboundGroupSession) Clear() error { + s.MegolmInboundSession = session.MegolmInboundSession{} + return nil +} + +// Pickle returns an InboundGroupSession as a base64 string. Encrypts the +// InboundGroupSession using the supplied key. +func (s *InboundGroupSession) Pickle(key []byte) []byte { + if len(key) == 0 { + panic(NoKeyProvided) + } + pickled, err := s.MegolmInboundSession.Pickle(key) + if err != nil { + panic(err) + } + return pickled +} + +func (s *InboundGroupSession) Unpickle(pickled, key []byte) error { + if len(key) == 0 { + return NoKeyProvided + } else if len(pickled) == 0 { + return EmptyInput + } + sOlm, err := session.MegolmInboundSessionFromPickled(pickled, key) + if err != nil { + return err + } + s.MegolmInboundSession = *sOlm + return nil +} + +// Decrypt decrypts a message using the InboundGroupSession. Returns the the +// plain-text and message index on success. Returns error on failure. +func (s *InboundGroupSession) Decrypt(message []byte) ([]byte, uint, error) { + if len(message) == 0 { + return nil, 0, EmptyInput + } + plaintext, messageIndex, err := s.MegolmInboundSession.Decrypt(message) + if err != nil { + return nil, 0, err + } + return plaintext, uint(messageIndex), nil +} + +// ID returns a base64-encoded identifier for this session. +func (s *InboundGroupSession) ID() id.SessionID { + return s.MegolmInboundSession.SessionID() +} + +// FirstKnownIndex returns the first message index we know how to decrypt. +func (s *InboundGroupSession) FirstKnownIndex() uint32 { + return s.MegolmInboundSession.InitialRatchet.Counter +} + +// IsVerified check if the session has been verified as a valid session. (A +// session is verified either because the original session share was signed, or +// because we have subsequently successfully decrypted a message.) +func (s *InboundGroupSession) IsVerified() uint { + if s.MegolmInboundSession.SigningKeyVerified { + return 1 + } + return 0 +} + +// Export returns the base64-encoded ratchet key for this session, at the given +// index, in a format which can be used by +// InboundGroupSession.InboundGroupSessionImport(). Encrypts the +// InboundGroupSession using the supplied key. Returns error on failure. +// if we do not have a session key corresponding to the given index (ie, it was +// sent before the session key was shared with us) the error will be +// returned. +func (s *InboundGroupSession) Export(messageIndex uint32) ([]byte, error) { + res, err := s.MegolmInboundSession.SessionExportMessage(messageIndex) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/olm.go b/vendor/maunium.net/go/mautrix/crypto/olm/olm.go index feb46e5..685e1b6 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/olm.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/olm.go @@ -1,3 +1,5 @@ +//go:build !goolm + package olm // #cgo LDFLAGS: -lolm -lstdc++ diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/olm_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/olm_goolm.go new file mode 100644 index 0000000..dbe12a7 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/olm_goolm.go @@ -0,0 +1,20 @@ +//go:build goolm + +package olm + +import ( + "maunium.net/go/mautrix/id" +) + +// Signatures is the data structure used to sign JSON objects. +type Signatures map[id.UserID]map[id.DeviceKeyID]string + +// Version returns the version number of the olm library. +func Version() (major, minor, patch uint8) { + return 3, 2, 15 +} + +// SetPickleKey sets the global pickle key used when encoding structs with Gob or JSON. +func SetPickleKey(key []byte) { + panic("gob and json encoding is deprecated and not supported with goolm") +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go b/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go index 5f7da05..b6a33d3 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go @@ -1,3 +1,5 @@ +//go:build !goolm + package olm // #cgo LDFLAGS: -lolm -lstdc++ @@ -122,6 +124,7 @@ func (s *OutboundGroupSession) Unpickle(pickled, key []byte) error { return nil } +// Deprecated func (s *OutboundGroupSession) GobEncode() ([]byte, error) { pickled := s.Pickle(pickleKey) length := base64.RawStdEncoding.DecodedLen(len(pickled)) @@ -130,6 +133,7 @@ func (s *OutboundGroupSession) GobEncode() ([]byte, error) { return rawPickled, err } +// Deprecated func (s *OutboundGroupSession) GobDecode(rawPickled []byte) error { if s == nil || s.int == nil { *s = *NewBlankOutboundGroupSession() @@ -140,6 +144,7 @@ func (s *OutboundGroupSession) GobDecode(rawPickled []byte) error { return s.Unpickle(pickled, pickleKey) } +// Deprecated func (s *OutboundGroupSession) MarshalJSON() ([]byte, error) { pickled := s.Pickle(pickleKey) quotes := make([]byte, len(pickled)+2) @@ -149,6 +154,7 @@ func (s *OutboundGroupSession) MarshalJSON() ([]byte, error) { return quotes, nil } +// Deprecated func (s *OutboundGroupSession) UnmarshalJSON(data []byte) error { if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' { return InputNotJSONString diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession_goolm.go new file mode 100644 index 0000000..7c20121 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession_goolm.go @@ -0,0 +1,111 @@ +//go:build goolm + +package olm + +import ( + "maunium.net/go/mautrix/crypto/goolm/session" + "maunium.net/go/mautrix/id" +) + +// OutboundGroupSession stores an outbound encrypted messaging session for a +// group. +type OutboundGroupSession struct { + session.MegolmOutboundSession +} + +// OutboundGroupSessionFromPickled loads an OutboundGroupSession from a pickled +// base64 string. Decrypts the OutboundGroupSession using the supplied key. +// Returns error on failure. If the key doesn't match the one used to encrypt +// the OutboundGroupSession then the error will be "BAD_SESSION_KEY". If the +// base64 couldn't be decoded then the error will be "INVALID_BASE64". +func OutboundGroupSessionFromPickled(pickled, key []byte) (*OutboundGroupSession, error) { + if len(pickled) == 0 { + return nil, EmptyInput + } + lenKey := len(key) + if lenKey == 0 { + key = []byte(" ") + } + megolmSession, err := session.MegolmOutboundSessionFromPickled(pickled, key) + if err != nil { + return nil, err + } + return &OutboundGroupSession{ + MegolmOutboundSession: *megolmSession, + }, nil +} + +// NewOutboundGroupSession creates a new outbound group session. +func NewOutboundGroupSession() *OutboundGroupSession { + megolmSession, err := session.NewMegolmOutboundSession() + if err != nil { + panic(err) + } + return &OutboundGroupSession{ + MegolmOutboundSession: *megolmSession, + } +} + +// newOutboundGroupSession initialises an empty OutboundGroupSession. +func NewBlankOutboundGroupSession() *OutboundGroupSession { + return &OutboundGroupSession{} +} + +// Clear clears the memory used to back this OutboundGroupSession. +func (s *OutboundGroupSession) Clear() error { + s.MegolmOutboundSession = session.MegolmOutboundSession{} + return nil +} + +// Pickle returns an OutboundGroupSession as a base64 string. Encrypts the +// OutboundGroupSession using the supplied key. +func (s *OutboundGroupSession) Pickle(key []byte) []byte { + if len(key) == 0 { + panic(NoKeyProvided) + } + pickled, err := s.MegolmOutboundSession.Pickle(key) + if err != nil { + panic(err) + } + return pickled +} + +func (s *OutboundGroupSession) Unpickle(pickled, key []byte) error { + if len(key) == 0 { + return NoKeyProvided + } + return s.MegolmOutboundSession.Unpickle(pickled, key) +} + +// Encrypt encrypts a message using the Session. Returns the encrypted message +// as base64. +func (s *OutboundGroupSession) Encrypt(plaintext []byte) []byte { + if len(plaintext) == 0 { + panic(EmptyInput) + } + message, err := s.MegolmOutboundSession.Encrypt(plaintext) + if err != nil { + panic(err) + } + return message +} + +// ID returns a base64-encoded identifier for this session. +func (s *OutboundGroupSession) ID() id.SessionID { + return s.MegolmOutboundSession.SessionID() +} + +// MessageIndex returns the message index for this session. Each message is +// sent with an increasing index; this returns the index for the next message. +func (s *OutboundGroupSession) MessageIndex() uint { + return uint(s.MegolmOutboundSession.Ratchet.Counter) +} + +// Key returns the base64-encoded current ratchet key for this session. +func (s *OutboundGroupSession) Key() string { + message, err := s.MegolmOutboundSession.SessionSharingMessage() + if err != nil { + panic(err) + } + return string(message) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/pk.go b/vendor/maunium.net/go/mautrix/crypto/olm/pk.go index e441ba1..ba390af 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/pk.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/pk.go @@ -1,3 +1,5 @@ +//go:build !goolm + package olm // #cgo LDFLAGS: -lolm -lstdc++ diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/pk_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/pk_goolm.go new file mode 100644 index 0000000..9659e91 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/pk_goolm.go @@ -0,0 +1,71 @@ +//go:build goolm + +package olm + +import ( + "encoding/json" + + "github.com/tidwall/sjson" + + "maunium.net/go/mautrix/crypto/canonicaljson" + "maunium.net/go/mautrix/crypto/goolm/pk" + "maunium.net/go/mautrix/id" +) + +// PkSigning stores a key pair for signing messages. +type PkSigning struct { + pk.Signing + PublicKey id.Ed25519 + Seed []byte +} + +// Clear clears the underlying memory of a PkSigning object. +func (p *PkSigning) Clear() { + p.Signing = pk.Signing{} +} + +// NewPkSigningFromSeed creates a new PkSigning object using the given seed. +func NewPkSigningFromSeed(seed []byte) (*PkSigning, error) { + p := &PkSigning{} + signing, err := pk.NewSigningFromSeed(seed) + if err != nil { + return nil, err + } + p.Signing = *signing + p.Seed = seed + p.PublicKey = p.Signing.PublicKey() + return p, nil +} + +// NewPkSigning creates a new PkSigning object, containing a key pair for signing messages. +func NewPkSigning() (*PkSigning, error) { + p := &PkSigning{} + signing, err := pk.NewSigning() + if err != nil { + return nil, err + } + p.Signing = *signing + p.Seed = signing.Seed + p.PublicKey = p.Signing.PublicKey() + return p, err +} + +// Sign creates a signature for the given message using this key. +func (p *PkSigning) Sign(message []byte) ([]byte, error) { + return p.Signing.Sign(message), nil +} + +// SignJSON creates a signature for the given object after encoding it to canonical JSON. +func (p *PkSigning) SignJSON(obj interface{}) (string, error) { + objJSON, err := json.Marshal(obj) + if err != nil { + return "", err + } + objJSON, _ = sjson.DeleteBytes(objJSON, "unsigned") + objJSON, _ = sjson.DeleteBytes(objJSON, "signatures") + signature, err := p.Sign(canonicaljson.CanonicalJSONAssumeValid(objJSON)) + if err != nil { + return "", err + } + return string(signature), nil +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/session.go b/vendor/maunium.net/go/mautrix/crypto/olm/session.go index 625a16c..185e0b3 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/session.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/session.go @@ -1,3 +1,5 @@ +//go:build !goolm + package olm // #cgo LDFLAGS: -lolm -lstdc++ @@ -155,6 +157,7 @@ func (s *Session) Unpickle(pickled, key []byte) error { return nil } +// Deprecated func (s *Session) GobEncode() ([]byte, error) { pickled := s.Pickle(pickleKey) length := base64.RawStdEncoding.DecodedLen(len(pickled)) @@ -163,6 +166,7 @@ func (s *Session) GobEncode() ([]byte, error) { return rawPickled, err } +// Deprecated func (s *Session) GobDecode(rawPickled []byte) error { if s == nil || s.int == nil { *s = *NewBlankSession() @@ -173,6 +177,7 @@ func (s *Session) GobDecode(rawPickled []byte) error { return s.Unpickle(pickled, pickleKey) } +// Deprecated func (s *Session) MarshalJSON() ([]byte, error) { pickled := s.Pickle(pickleKey) quotes := make([]byte, len(pickled)+2) @@ -182,6 +187,7 @@ func (s *Session) MarshalJSON() ([]byte, error) { return quotes, nil } +// Deprecated func (s *Session) UnmarshalJSON(data []byte) error { if len(data) == 0 || len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' { return InputNotJSONString diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/session_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/session_goolm.go new file mode 100644 index 0000000..c77efaa --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/session_goolm.go @@ -0,0 +1,110 @@ +//go:build goolm + +package olm + +import ( + "maunium.net/go/mautrix/crypto/goolm/session" + "maunium.net/go/mautrix/id" +) + +// Session stores an end to end encrypted messaging session. +type Session struct { + session.OlmSession +} + +// SessionFromPickled loads a Session from a pickled base64 string. Decrypts +// the Session using the supplied key. Returns error on failure. +func SessionFromPickled(pickled, key []byte) (*Session, error) { + if len(pickled) == 0 { + return nil, EmptyInput + } + s := NewBlankSession() + return s, s.Unpickle(pickled, key) +} + +func NewBlankSession() *Session { + return &Session{} +} + +// Clear clears the memory used to back this Session. +func (s *Session) Clear() error { + s.OlmSession = session.OlmSession{} + return nil +} + +// Pickle returns a Session as a base64 string. Encrypts the Session using the +// supplied key. +func (s *Session) Pickle(key []byte) []byte { + if len(key) == 0 { + panic(NoKeyProvided) + } + pickled, err := s.OlmSession.Pickle(key) + if err != nil { + panic(err) + } + return pickled +} + +func (s *Session) Unpickle(pickled, key []byte) error { + if len(key) == 0 { + return NoKeyProvided + } else if len(pickled) == 0 { + return EmptyInput + } + sOlm, err := session.OlmSessionFromPickled(pickled, key) + if err != nil { + return err + } + s.OlmSession = *sOlm + return nil +} + +// MatchesInboundSession checks if the PRE_KEY message is for this in-bound +// Session. This can happen if multiple messages are sent to this Account +// before this Account sends a message in reply. Returns true if the session +// matches. Returns false if the session does not match. Returns error on +// failure. +func (s *Session) MatchesInboundSession(oneTimeKeyMsg string) (bool, error) { + return s.MatchesInboundSessionFrom("", oneTimeKeyMsg) +} + +// MatchesInboundSessionFrom checks if the PRE_KEY message is for this in-bound +// Session. This can happen if multiple messages are sent to this Account +// before this Account sends a message in reply. Returns true if the session +// matches. Returns false if the session does not match. Returns error on +// failure. +func (s *Session) MatchesInboundSessionFrom(theirIdentityKey, oneTimeKeyMsg string) (bool, error) { + if theirIdentityKey != "" { + theirKey := id.Curve25519(theirIdentityKey) + return s.OlmSession.MatchesInboundSessionFrom(&theirKey, []byte(oneTimeKeyMsg)) + } + return s.OlmSession.MatchesInboundSessionFrom(nil, []byte(oneTimeKeyMsg)) + +} + +// Encrypt encrypts a message using the Session. Returns the encrypted message +// as base64. +func (s *Session) Encrypt(plaintext []byte) (id.OlmMsgType, []byte) { + if len(plaintext) == 0 { + panic(EmptyInput) + } + messageType, message, err := s.OlmSession.Encrypt(plaintext, nil) + if err != nil { + panic(err) + } + return messageType, message +} + +// Decrypt decrypts a message using the Session. Returns the the plain-text on +// success. Returns error on failure. +func (s *Session) Decrypt(message string, msgType id.OlmMsgType) ([]byte, error) { + if len(message) == 0 { + return nil, EmptyInput + } + return s.OlmSession.Decrypt([]byte(message), msgType) +} + +// Describe generates a string describing the internal state of an olm session for debugging and logging purposes. +func (s *Session) Describe() string { + return s.OlmSession.Describe() +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/utility.go b/vendor/maunium.net/go/mautrix/crypto/olm/utility.go index 8e868de..87055fb 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/utility.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/utility.go @@ -1,3 +1,5 @@ +//go:build !goolm + package olm // #cgo LDFLAGS: -lolm -lstdc++ diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/utility_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/utility_goolm.go new file mode 100644 index 0000000..926b540 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/utility_goolm.go @@ -0,0 +1,92 @@ +//go:build goolm + +package olm + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + "go.mau.fi/util/exgjson" + + "maunium.net/go/mautrix/crypto/canonicaljson" + "maunium.net/go/mautrix/crypto/goolm/utilities" + "maunium.net/go/mautrix/id" +) + +// Utility stores the necessary state to perform hash and signature +// verification operations. +type Utility struct{} + +// Clear clears the memory used to back this utility. +func (u *Utility) Clear() error { + return nil +} + +// NewUtility creates a new utility. +func NewUtility() *Utility { + return &Utility{} +} + +// Sha256 calculates the SHA-256 hash of the input and encodes it as base64. +func (u *Utility) Sha256(input string) string { + if len(input) == 0 { + panic(EmptyInput) + } + hash := sha256.Sum256([]byte(input)) + return base64.RawStdEncoding.EncodeToString(hash[:]) +} + +// VerifySignature verifies an ed25519 signature. Returns true if the verification +// suceeds or false otherwise. Returns error on failure. If the key was too +// small then the error will be "INVALID_BASE64". +func (u *Utility) VerifySignature(message string, key id.Ed25519, signature string) (ok bool, err error) { + if len(message) == 0 || len(key) == 0 || len(signature) == 0 { + return false, EmptyInput + } + return utilities.VerifySignature([]byte(message), key, []byte(signature)) +} + +// VerifySignatureJSON verifies the signature in the JSON object _obj following +// the Matrix specification: +// https://matrix.org/speculator/spec/drafts%2Fe2e/appendices.html#signing-json +// If the _obj is a struct, the `json` tags will be honored. +func (u *Utility) VerifySignatureJSON(obj interface{}, userID id.UserID, keyName string, key id.Ed25519) (bool, error) { + var err error + objJSON, ok := obj.(json.RawMessage) + if !ok { + objJSON, err = json.Marshal(obj) + if err != nil { + return false, err + } + } + sig := gjson.GetBytes(objJSON, exgjson.Path("signatures", string(userID), fmt.Sprintf("ed25519:%s", keyName))) + if !sig.Exists() || sig.Type != gjson.String { + return false, SignatureNotFound + } + objJSON, err = sjson.DeleteBytes(objJSON, "unsigned") + if err != nil { + return false, err + } + objJSON, err = sjson.DeleteBytes(objJSON, "signatures") + if err != nil { + return false, err + } + objJSONString := string(canonicaljson.CanonicalJSONAssumeValid(objJSON)) + return u.VerifySignature(objJSONString, key, sig.Str) +} + +// VerifySignatureJSON verifies the signature in the JSON object _obj following +// the Matrix specification: +// https://matrix.org/speculator/spec/drafts%2Fe2e/appendices.html#signing-json +// This function is a wrapper over Utility.VerifySignatureJSON that creates and +// destroys the Utility object transparently. +// If the _obj is a struct, the `json` tags will be honored. +func VerifySignatureJSON(obj interface{}, userID id.UserID, keyName string, key id.Ed25519) (bool, error) { + u := NewUtility() + defer u.Clear() + return u.VerifySignatureJSON(obj, userID, keyName, key) +} diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/verification.go b/vendor/maunium.net/go/mautrix/crypto/olm/verification.go index 4a363a0..bb0db7b 100644 --- a/vendor/maunium.net/go/mautrix/crypto/olm/verification.go +++ b/vendor/maunium.net/go/mautrix/crypto/olm/verification.go @@ -1,3 +1,5 @@ +//go:build !nosas && !goolm + package olm // #cgo LDFLAGS: -lolm -lstdc++ diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/verification_goolm.go b/vendor/maunium.net/go/mautrix/crypto/olm/verification_goolm.go new file mode 100644 index 0000000..fab51e5 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/olm/verification_goolm.go @@ -0,0 +1,23 @@ +//go:build !nosas && goolm + +package olm + +import ( + "maunium.net/go/mautrix/crypto/goolm/sas" +) + +// SAS stores an Olm Short Authentication String (SAS) object. +type SAS struct { + sas.SAS +} + +// NewSAS creates a new SAS object. +func NewSAS() *SAS { + newSAS, err := sas.New() + if err != nil { + panic(err) + } + return &SAS{ + SAS: *newSAS, + } +} diff --git a/vendor/maunium.net/go/mautrix/crypto/sql_store.go b/vendor/maunium.net/go/mautrix/crypto/sql_store.go index 15709bd..99a94f0 100644 --- a/vendor/maunium.net/go/mautrix/crypto/sql_store.go +++ b/vendor/maunium.net/go/mautrix/crypto/sql_store.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -27,7 +27,7 @@ import ( "maunium.net/go/mautrix/id" ) -var PostgresArrayWrapper func(interface{}) interface { +var PostgresArrayWrapper func(any) interface { driver.Valuer sql.Scanner } @@ -62,22 +62,22 @@ func NewSQLCryptoStore(db *dbutil.Database, log dbutil.DatabaseLogger, accountID } // Flush does nothing for this implementation as data is already persisted in the database. -func (store *SQLCryptoStore) Flush() error { +func (store *SQLCryptoStore) Flush(_ context.Context) error { return nil } // PutNextBatch stores the next sync batch token for the current account. -func (store *SQLCryptoStore) PutNextBatch(nextBatch string) error { +func (store *SQLCryptoStore) PutNextBatch(ctx context.Context, nextBatch string) error { store.SyncToken = nextBatch - _, err := store.DB.Exec(`UPDATE crypto_account SET sync_token=$1 WHERE account_id=$2`, store.SyncToken, store.AccountID) + _, err := store.DB.Exec(ctx, `UPDATE crypto_account SET sync_token=$1 WHERE account_id=$2`, store.SyncToken, store.AccountID) return err } // GetNextBatch retrieves the next sync batch token for the current account. -func (store *SQLCryptoStore) GetNextBatch() (string, error) { +func (store *SQLCryptoStore) GetNextBatch(ctx context.Context) (string, error) { if store.SyncToken == "" { - err := store.DB. - QueryRow("SELECT sync_token FROM crypto_account WHERE account_id=$1", store.AccountID). + err := store.DB.Conn(ctx). + QueryRowContext(ctx, "SELECT sync_token FROM crypto_account WHERE account_id=$1", store.AccountID). Scan(&store.SyncToken) if !errors.Is(err, sql.ErrNoRows) { return "", err @@ -88,38 +88,42 @@ func (store *SQLCryptoStore) GetNextBatch() (string, error) { var _ mautrix.SyncStore = (*SQLCryptoStore)(nil) -func (store *SQLCryptoStore) SaveFilterID(_ id.UserID, _ string) {} -func (store *SQLCryptoStore) LoadFilterID(_ id.UserID) string { return "" } - -func (store *SQLCryptoStore) SaveNextBatch(_ id.UserID, nextBatchToken string) { - err := store.PutNextBatch(nextBatchToken) - if err != nil { - // TODO handle error - } +func (store *SQLCryptoStore) SaveFilterID(ctx context.Context, _ id.UserID, _ string) error { + return nil +} +func (store *SQLCryptoStore) LoadFilterID(ctx context.Context, _ id.UserID) (string, error) { + return "", nil } -func (store *SQLCryptoStore) LoadNextBatch(_ id.UserID) string { - nb, err := store.GetNextBatch() +func (store *SQLCryptoStore) SaveNextBatch(ctx context.Context, _ id.UserID, nextBatchToken string) error { + err := store.PutNextBatch(ctx, nextBatchToken) if err != nil { - // TODO handle error + return fmt.Errorf("unable to store batch: %w", err) } - return nb + return nil } -func (store *SQLCryptoStore) FindDeviceID() (deviceID id.DeviceID) { - err := store.DB.QueryRow("SELECT device_id FROM crypto_account WHERE account_id=$1", store.AccountID).Scan(&deviceID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - // TODO return error - store.DB.Log.Warn("Failed to scan device ID: %v", err) +func (store *SQLCryptoStore) LoadNextBatch(ctx context.Context, _ id.UserID) (string, error) { + nb, err := store.GetNextBatch(ctx) + if err != nil { + return "", fmt.Errorf("unable to load batch: %w", err) + } + return nb, nil +} + +func (store *SQLCryptoStore) FindDeviceID(ctx context.Context) (deviceID id.DeviceID, err error) { + err = store.DB.QueryRow(ctx, "SELECT device_id FROM crypto_account WHERE account_id=$1", store.AccountID).Scan(&deviceID) + if errors.Is(err, sql.ErrNoRows) { + err = nil } return } // PutAccount stores an OlmAccount in the database. -func (store *SQLCryptoStore) PutAccount(account *OlmAccount) error { +func (store *SQLCryptoStore) PutAccount(ctx context.Context, account *OlmAccount) error { store.Account = account bytes := account.Internal.Pickle(store.PickleKey) - _, err := store.DB.Exec(` + _, err := store.DB.Exec(ctx, ` INSERT INTO crypto_account (device_id, shared, sync_token, account, account_id) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (account_id) DO UPDATE SET shared=excluded.shared, sync_token=excluded.sync_token, account=excluded.account, account_id=excluded.account_id @@ -128,9 +132,9 @@ func (store *SQLCryptoStore) PutAccount(account *OlmAccount) error { } // GetAccount retrieves an OlmAccount from the database. -func (store *SQLCryptoStore) GetAccount() (*OlmAccount, error) { +func (store *SQLCryptoStore) GetAccount(ctx context.Context) (*OlmAccount, error) { if store.Account == nil { - row := store.DB.QueryRow("SELECT shared, sync_token, account FROM crypto_account WHERE account_id=$1", store.AccountID) + row := store.DB.QueryRow(ctx, "SELECT shared, sync_token, account FROM crypto_account WHERE account_id=$1", store.AccountID) acc := &OlmAccount{Internal: *olm.NewBlankAccount()} var accountBytes []byte err := row.Scan(&acc.Shared, &store.SyncToken, &accountBytes) @@ -149,7 +153,7 @@ func (store *SQLCryptoStore) GetAccount() (*OlmAccount, error) { } // HasSession returns whether there is an Olm session for the given sender key. -func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool { +func (store *SQLCryptoStore) HasSession(ctx context.Context, key id.SenderKey) bool { store.olmSessionCacheLock.Lock() cache, ok := store.olmSessionCache[key] store.olmSessionCacheLock.Unlock() @@ -157,17 +161,17 @@ func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool { return true } var sessionID id.SessionID - err := store.DB.QueryRow("SELECT session_id FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 LIMIT 1", + err := store.DB.QueryRow(ctx, "SELECT session_id FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 LIMIT 1", key, store.AccountID).Scan(&sessionID) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return false } return len(sessionID) > 0 } // GetSessions returns all the known Olm sessions for a sender key. -func (store *SQLCryptoStore) GetSessions(key id.SenderKey) (OlmSessionList, error) { - rows, err := store.DB.Query("SELECT session_id, session, created_at, last_encrypted, last_decrypted FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY last_decrypted DESC", +func (store *SQLCryptoStore) GetSessions(ctx context.Context, key id.SenderKey) (OlmSessionList, error) { + rows, err := store.DB.Query(ctx, "SELECT session_id, session, created_at, last_encrypted, last_decrypted FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY last_decrypted DESC", key, store.AccountID) if err != nil { return nil, err @@ -207,11 +211,11 @@ func (store *SQLCryptoStore) getOlmSessionCache(key id.SenderKey) map[id.Session } // GetLatestSession retrieves the Olm session for a given sender key from the database that has the largest ID. -func (store *SQLCryptoStore) GetLatestSession(key id.SenderKey) (*OlmSession, error) { +func (store *SQLCryptoStore) GetLatestSession(ctx context.Context, key id.SenderKey) (*OlmSession, error) { store.olmSessionCacheLock.Lock() defer store.olmSessionCacheLock.Unlock() - row := store.DB.QueryRow("SELECT session_id, session, created_at, last_encrypted, last_decrypted FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY last_decrypted DESC LIMIT 1", + row := store.DB.QueryRow(ctx, "SELECT session_id, session, created_at, last_encrypted, last_decrypted FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY last_decrypted DESC LIMIT 1", key, store.AccountID) sess := OlmSession{Internal: *olm.NewBlankSession()} @@ -219,7 +223,7 @@ func (store *SQLCryptoStore) GetLatestSession(key id.SenderKey) (*OlmSession, er var sessionID id.SessionID err := row.Scan(&sessionID, &sessionBytes, &sess.CreationTime, &sess.LastEncryptedTime, &sess.LastDecryptedTime) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } else if err != nil { return nil, err @@ -237,20 +241,20 @@ func (store *SQLCryptoStore) GetLatestSession(key id.SenderKey) (*OlmSession, er } // AddSession persists an Olm session for a sender in the database. -func (store *SQLCryptoStore) AddSession(key id.SenderKey, session *OlmSession) error { +func (store *SQLCryptoStore) AddSession(ctx context.Context, key id.SenderKey, session *OlmSession) error { store.olmSessionCacheLock.Lock() defer store.olmSessionCacheLock.Unlock() sessionBytes := session.Internal.Pickle(store.PickleKey) - _, err := store.DB.Exec("INSERT INTO crypto_olm_session (session_id, sender_key, session, created_at, last_encrypted, last_decrypted, account_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", + _, err := store.DB.Exec(ctx, "INSERT INTO crypto_olm_session (session_id, sender_key, session, created_at, last_encrypted, last_decrypted, account_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", session.ID(), key, sessionBytes, session.CreationTime, session.LastEncryptedTime, session.LastDecryptedTime, store.AccountID) store.getOlmSessionCache(key)[session.ID()] = session return err } // UpdateSession replaces the Olm session for a sender in the database. -func (store *SQLCryptoStore) UpdateSession(_ id.SenderKey, session *OlmSession) error { +func (store *SQLCryptoStore) UpdateSession(ctx context.Context, _ id.SenderKey, session *OlmSession) error { sessionBytes := session.Internal.Pickle(store.PickleKey) - _, err := store.DB.Exec("UPDATE crypto_olm_session SET session=$1, last_encrypted=$2, last_decrypted=$3 WHERE session_id=$4 AND account_id=$5", + _, err := store.DB.Exec(ctx, "UPDATE crypto_olm_session SET session=$1, last_encrypted=$2, last_decrypted=$3 WHERE session_id=$4 AND account_id=$5", sessionBytes, session.LastEncryptedTime, session.LastDecryptedTime, session.ID(), store.AccountID) return err } @@ -270,14 +274,14 @@ func datePtr(t time.Time) *time.Time { } // PutGroupSession stores an inbound Megolm group session for a room, sender and session. -func (store *SQLCryptoStore) PutGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, session *InboundGroupSession) error { +func (store *SQLCryptoStore) PutGroupSession(ctx context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, session *InboundGroupSession) error { sessionBytes := session.Internal.Pickle(store.PickleKey) forwardingChains := strings.Join(session.ForwardingChains, ",") ratchetSafety, err := json.Marshal(&session.RatchetSafety) if err != nil { return fmt.Errorf("failed to marshal ratchet safety info: %w", err) } - _, err = store.DB.Exec(` + _, err = store.DB.Exec(ctx, ` INSERT INTO crypto_megolm_inbound_session ( session_id, sender_key, signing_key, room_id, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, account_id @@ -296,19 +300,19 @@ func (store *SQLCryptoStore) PutGroupSession(roomID id.RoomID, senderKey id.Send } // GetGroupSession retrieves an inbound Megolm group session for a room, sender and session. -func (store *SQLCryptoStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*InboundGroupSession, error) { +func (store *SQLCryptoStore) GetGroupSession(ctx context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*InboundGroupSession, error) { var senderKeyDB, signingKey, forwardingChains, withheldCode, withheldReason sql.NullString var sessionBytes, ratchetSafetyBytes []byte var receivedAt sql.NullTime var maxAge, maxMessages sql.NullInt64 var isScheduled bool - err := store.DB.QueryRow(` + err := store.DB.QueryRow(ctx, ` SELECT sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled FROM crypto_megolm_inbound_session WHERE room_id=$1 AND (sender_key=$2 OR $2 = '') AND session_id=$3 AND account_id=$4`, roomID, senderKey, sessionID, store.AccountID, ).Scan(&senderKeyDB, &signingKey, &sessionBytes, &forwardingChains, &withheldCode, &withheldReason, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } else if err != nil { return nil, err @@ -322,22 +326,7 @@ func (store *SQLCryptoStore) GetGroupSession(roomID id.RoomID, senderKey id.Send Reason: withheldReason.String, } } - igs := olm.NewBlankInboundGroupSession() - err = igs.Unpickle(sessionBytes, store.PickleKey) - if err != nil { - return nil, err - } - var chains []string - if forwardingChains.String != "" { - chains = strings.Split(forwardingChains.String, ",") - } - var rs RatchetSafety - if len(ratchetSafetyBytes) > 0 { - err = json.Unmarshal(ratchetSafetyBytes, &rs) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal ratchet safety info: %w", err) - } - } + igs, chains, rs, err := store.postScanInboundGroupSession(sessionBytes, ratchetSafetyBytes, forwardingChains.String) if senderKey == "" { senderKey = id.Curve25519(senderKeyDB.String) } @@ -355,8 +344,8 @@ func (store *SQLCryptoStore) GetGroupSession(roomID id.RoomID, senderKey id.Send }, nil } -func (store *SQLCryptoStore) RedactGroupSession(_ id.RoomID, _ id.SenderKey, sessionID id.SessionID, reason string) error { - _, err := store.DB.Exec(` +func (store *SQLCryptoStore) RedactGroupSession(ctx context.Context, _ id.RoomID, _ id.SenderKey, sessionID id.SessionID, reason string) error { + _, err := store.DB.Exec(ctx, ` UPDATE crypto_megolm_inbound_session SET withheld_code=$1, withheld_reason=$2, session=NULL, forwarding_chains=NULL WHERE session_id=$3 AND account_id=$4 AND session IS NOT NULL @@ -364,27 +353,24 @@ func (store *SQLCryptoStore) RedactGroupSession(_ id.RoomID, _ id.SenderKey, ses return err } -func (store *SQLCryptoStore) RedactGroupSessions(roomID id.RoomID, senderKey id.SenderKey, reason string) ([]id.SessionID, error) { +func (store *SQLCryptoStore) RedactGroupSessions(ctx context.Context, roomID id.RoomID, senderKey id.SenderKey, reason string) ([]id.SessionID, error) { if roomID == "" && senderKey == "" { return nil, fmt.Errorf("room ID or sender key must be provided for redacting sessions") } - res, err := store.DB.Query(` + res, err := store.DB.Query(ctx, ` UPDATE crypto_megolm_inbound_session SET withheld_code=$1, withheld_reason=$2, session=NULL, forwarding_chains=NULL WHERE (room_id=$3 OR $3='') AND (sender_key=$4 OR $4='') AND account_id=$5 AND session IS NOT NULL AND is_scheduled=false AND received_at IS NOT NULL RETURNING session_id `, event.RoomKeyWithheldBeeperRedacted, "Session redacted: "+reason, roomID, senderKey, store.AccountID) - var sessionIDs []id.SessionID - for res.Next() { - var sessionID id.SessionID - _ = res.Scan(&sessionID) - sessionIDs = append(sessionIDs, sessionID) + if err != nil { + return nil, err } - return sessionIDs, err + return dbutil.NewRowIter(res, dbutil.ScanSingleColumn[id.SessionID]).AsList() } -func (store *SQLCryptoStore) RedactExpiredGroupSessions() ([]id.SessionID, error) { +func (store *SQLCryptoStore) RedactExpiredGroupSessions(ctx context.Context) ([]id.SessionID, error) { var query string switch store.DB.Dialect { case dbutil.Postgres: @@ -408,46 +394,40 @@ func (store *SQLCryptoStore) RedactExpiredGroupSessions() ([]id.SessionID, error default: return nil, fmt.Errorf("unsupported dialect") } - res, err := store.DB.Query(query, event.RoomKeyWithheldBeeperRedacted, "Session redacted: expired", store.AccountID) - var sessionIDs []id.SessionID - for res.Next() { - var sessionID id.SessionID - _ = res.Scan(&sessionID) - sessionIDs = append(sessionIDs, sessionID) + res, err := store.DB.Query(ctx, query, event.RoomKeyWithheldBeeperRedacted, "Session redacted: expired", store.AccountID) + if err != nil { + return nil, err } - return sessionIDs, err + return dbutil.NewRowIter(res, dbutil.ScanSingleColumn[id.SessionID]).AsList() } -func (store *SQLCryptoStore) RedactOutdatedGroupSessions() ([]id.SessionID, error) { - res, err := store.DB.Query(` +func (store *SQLCryptoStore) RedactOutdatedGroupSessions(ctx context.Context) ([]id.SessionID, error) { + res, err := store.DB.Query(ctx, ` UPDATE crypto_megolm_inbound_session SET withheld_code=$1, withheld_reason=$2, session=NULL, forwarding_chains=NULL WHERE account_id=$3 AND session IS NOT NULL AND received_at IS NULL RETURNING session_id `, event.RoomKeyWithheldBeeperRedacted, "Session redacted: outdated", store.AccountID) - var sessionIDs []id.SessionID - for res.Next() { - var sessionID id.SessionID - _ = res.Scan(&sessionID) - sessionIDs = append(sessionIDs, sessionID) + if err != nil { + return nil, err } - return sessionIDs, err + return dbutil.NewRowIter(res, dbutil.ScanSingleColumn[id.SessionID]).AsList() } -func (store *SQLCryptoStore) PutWithheldGroupSession(content event.RoomKeyWithheldEventContent) error { - _, err := store.DB.Exec("INSERT INTO crypto_megolm_inbound_session (session_id, sender_key, room_id, withheld_code, withheld_reason, received_at, account_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", +func (store *SQLCryptoStore) PutWithheldGroupSession(ctx context.Context, content event.RoomKeyWithheldEventContent) error { + _, err := store.DB.Exec(ctx, "INSERT INTO crypto_megolm_inbound_session (session_id, sender_key, room_id, withheld_code, withheld_reason, received_at, account_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", content.SessionID, content.SenderKey, content.RoomID, content.Code, content.Reason, time.Now().UTC(), store.AccountID) return err } -func (store *SQLCryptoStore) GetWithheldGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*event.RoomKeyWithheldEventContent, error) { +func (store *SQLCryptoStore) GetWithheldGroupSession(ctx context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*event.RoomKeyWithheldEventContent, error) { var code, reason sql.NullString - err := store.DB.QueryRow(` + err := store.DB.QueryRow(ctx, ` SELECT withheld_code, withheld_reason FROM crypto_megolm_inbound_session WHERE room_id=$1 AND sender_key=$2 AND session_id=$3 AND account_id=$4`, roomID, senderKey, sessionID, store.AccountID, ).Scan(&code, &reason) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } else if err != nil || !code.Valid { return nil, err @@ -462,82 +442,79 @@ func (store *SQLCryptoStore) GetWithheldGroupSession(roomID id.RoomID, senderKey }, nil } -func (store *SQLCryptoStore) scanGroupSessionList(rows dbutil.Rows) (result []*InboundGroupSession, err error) { - for rows.Next() { - var roomID id.RoomID - var signingKey, senderKey, forwardingChains sql.NullString - var sessionBytes, ratchetSafetyBytes []byte - var receivedAt sql.NullTime - var maxAge, maxMessages sql.NullInt64 - var isScheduled bool - err = rows.Scan(&roomID, &signingKey, &senderKey, &sessionBytes, &forwardingChains, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled) +func (store *SQLCryptoStore) postScanInboundGroupSession(sessionBytes, ratchetSafetyBytes []byte, forwardingChains string) (igs *olm.InboundGroupSession, chains []string, safety RatchetSafety, err error) { + igs = olm.NewBlankInboundGroupSession() + err = igs.Unpickle(sessionBytes, store.PickleKey) + if err != nil { + return + } + if forwardingChains != "" { + chains = strings.Split(forwardingChains, ",") + } + var rs RatchetSafety + if len(ratchetSafetyBytes) > 0 { + err = json.Unmarshal(ratchetSafetyBytes, &rs) if err != nil { - return + err = fmt.Errorf("failed to unmarshal ratchet safety info: %w", err) } - igs := olm.NewBlankInboundGroupSession() - err = igs.Unpickle(sessionBytes, store.PickleKey) - if err != nil { - return - } - var chains []string - if forwardingChains.String != "" { - chains = strings.Split(forwardingChains.String, ",") - } - var rs RatchetSafety - if len(ratchetSafetyBytes) > 0 { - err = json.Unmarshal(ratchetSafetyBytes, &rs) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal ratchet safety info: %w", err) - } - } - result = append(result, &InboundGroupSession{ - Internal: *igs, - SigningKey: id.Ed25519(signingKey.String), - SenderKey: id.Curve25519(senderKey.String), - RoomID: roomID, - ForwardingChains: chains, - RatchetSafety: rs, - ReceivedAt: receivedAt.Time, - MaxAge: maxAge.Int64, - MaxMessages: int(maxMessages.Int64), - IsScheduled: isScheduled, - }) } return } -func (store *SQLCryptoStore) GetGroupSessionsForRoom(roomID id.RoomID) ([]*InboundGroupSession, error) { - rows, err := store.DB.Query(` - SELECT room_id, signing_key, sender_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled +func (store *SQLCryptoStore) scanInboundGroupSession(rows dbutil.Scannable) (*InboundGroupSession, error) { + var roomID id.RoomID + var signingKey, senderKey, forwardingChains sql.NullString + var sessionBytes, ratchetSafetyBytes []byte + var receivedAt sql.NullTime + var maxAge, maxMessages sql.NullInt64 + var isScheduled bool + err := rows.Scan(&roomID, &senderKey, &signingKey, &sessionBytes, &forwardingChains, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled) + if err != nil { + return nil, err + } + igs, chains, rs, err := store.postScanInboundGroupSession(sessionBytes, ratchetSafetyBytes, forwardingChains.String) + return &InboundGroupSession{ + Internal: *igs, + SigningKey: id.Ed25519(signingKey.String), + SenderKey: id.Curve25519(senderKey.String), + RoomID: roomID, + ForwardingChains: chains, + RatchetSafety: rs, + ReceivedAt: receivedAt.Time, + MaxAge: maxAge.Int64, + MaxMessages: int(maxMessages.Int64), + IsScheduled: isScheduled, + }, nil +} + +func (store *SQLCryptoStore) GetGroupSessionsForRoom(ctx context.Context, roomID id.RoomID) ([]*InboundGroupSession, error) { + rows, err := store.DB.Query(ctx, ` + SELECT room_id, sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled FROM crypto_megolm_inbound_session WHERE room_id=$1 AND account_id=$2 AND session IS NOT NULL`, roomID, store.AccountID, ) - if err == sql.ErrNoRows { - return []*InboundGroupSession{}, nil - } else if err != nil { + if err != nil { return nil, err } - return store.scanGroupSessionList(rows) + return dbutil.NewRowIter(rows, store.scanInboundGroupSession).AsList() } -func (store *SQLCryptoStore) GetAllGroupSessions() ([]*InboundGroupSession, error) { - rows, err := store.DB.Query(` - SELECT room_id, signing_key, sender_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled +func (store *SQLCryptoStore) GetAllGroupSessions(ctx context.Context) ([]*InboundGroupSession, error) { + rows, err := store.DB.Query(ctx, ` + SELECT room_id, sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled FROM crypto_megolm_inbound_session WHERE account_id=$2 AND session IS NOT NULL`, store.AccountID, ) - if err == sql.ErrNoRows { - return []*InboundGroupSession{}, nil - } else if err != nil { + if err != nil { return nil, err } - return store.scanGroupSessionList(rows) + return dbutil.NewRowIter(rows, store.scanInboundGroupSession).AsList() } // AddOutboundGroupSession stores an outbound Megolm session, along with the information about the room and involved devices. -func (store *SQLCryptoStore) AddOutboundGroupSession(session *OutboundGroupSession) error { +func (store *SQLCryptoStore) AddOutboundGroupSession(ctx context.Context, session *OutboundGroupSession) error { sessionBytes := session.Internal.Pickle(store.PickleKey) - _, err := store.DB.Exec(` + _, err := store.DB.Exec(ctx, ` INSERT INTO crypto_megolm_outbound_session (room_id, session_id, session, shared, max_messages, message_count, max_age, created_at, last_used, account_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) @@ -551,24 +528,24 @@ func (store *SQLCryptoStore) AddOutboundGroupSession(session *OutboundGroupSessi } // UpdateOutboundGroupSession replaces an outbound Megolm session with for same room and session ID. -func (store *SQLCryptoStore) UpdateOutboundGroupSession(session *OutboundGroupSession) error { +func (store *SQLCryptoStore) UpdateOutboundGroupSession(ctx context.Context, session *OutboundGroupSession) error { sessionBytes := session.Internal.Pickle(store.PickleKey) - _, err := store.DB.Exec("UPDATE crypto_megolm_outbound_session SET session=$1, message_count=$2, last_used=$3 WHERE room_id=$4 AND session_id=$5 AND account_id=$6", + _, err := store.DB.Exec(ctx, "UPDATE crypto_megolm_outbound_session SET session=$1, message_count=$2, last_used=$3 WHERE room_id=$4 AND session_id=$5 AND account_id=$6", sessionBytes, session.MessageCount, session.LastEncryptedTime, session.RoomID, session.ID(), store.AccountID) return err } // GetOutboundGroupSession retrieves the outbound Megolm session for the given room ID. -func (store *SQLCryptoStore) GetOutboundGroupSession(roomID id.RoomID) (*OutboundGroupSession, error) { +func (store *SQLCryptoStore) GetOutboundGroupSession(ctx context.Context, roomID id.RoomID) (*OutboundGroupSession, error) { var ogs OutboundGroupSession var sessionBytes []byte var maxAgeMS int64 - err := store.DB.QueryRow(` + err := store.DB.QueryRow(ctx, ` SELECT session, shared, max_messages, message_count, max_age, created_at, last_used FROM crypto_megolm_outbound_session WHERE room_id=$1 AND account_id=$2`, roomID, store.AccountID, ).Scan(&sessionBytes, &ogs.Shared, &ogs.MaxMessages, &ogs.MessageCount, &maxAgeMS, &ogs.CreationTime, &ogs.LastEncryptedTime) - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, nil } else if err != nil { return nil, err @@ -585,8 +562,8 @@ func (store *SQLCryptoStore) GetOutboundGroupSession(roomID id.RoomID) (*Outboun } // RemoveOutboundGroupSession removes the outbound Megolm session for the given room ID. -func (store *SQLCryptoStore) RemoveOutboundGroupSession(roomID id.RoomID) error { - _, err := store.DB.Exec("DELETE FROM crypto_megolm_outbound_session WHERE room_id=$1 AND account_id=$2", +func (store *SQLCryptoStore) RemoveOutboundGroupSession(ctx context.Context, roomID id.RoomID) error { + _, err := store.DB.Exec(ctx, "DELETE FROM crypto_megolm_outbound_session WHERE room_id=$1 AND account_id=$2", roomID, store.AccountID) return err } @@ -603,7 +580,7 @@ func (store *SQLCryptoStore) ValidateMessageIndex(ctx context.Context, senderKey ` var expectedEventID id.EventID var expectedTimestamp int64 - err := store.DB.QueryRowContext(ctx, validateQuery, senderKey, sessionID, index, eventID, timestamp).Scan(&expectedEventID, &expectedTimestamp) + err := store.DB.QueryRow(ctx, validateQuery, senderKey, sessionID, index, eventID, timestamp).Scan(&expectedEventID, &expectedTimestamp) if err != nil { return false, err } else if expectedEventID != eventID || expectedTimestamp != timestamp { @@ -618,69 +595,58 @@ func (store *SQLCryptoStore) ValidateMessageIndex(ctx context.Context, senderKey return true, nil } +func scanDevice(rows dbutil.Scannable) (*id.Device, error) { + var device id.Device + err := rows.Scan(&device.UserID, &device.DeviceID, &device.IdentityKey, &device.SigningKey, &device.Trust, &device.Deleted, &device.Name) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } else if err != nil { + return nil, err + } + return &device, nil +} + // GetDevices returns a map of device IDs to device identities, including the identity and signing keys, for a given user ID. -func (store *SQLCryptoStore) GetDevices(userID id.UserID) (map[id.DeviceID]*id.Device, error) { +func (store *SQLCryptoStore) GetDevices(ctx context.Context, userID id.UserID) (map[id.DeviceID]*id.Device, error) { var ignore id.UserID - err := store.DB.QueryRow("SELECT user_id FROM crypto_tracked_user WHERE user_id=$1", userID).Scan(&ignore) - if err == sql.ErrNoRows { + err := store.DB.QueryRow(ctx, "SELECT user_id FROM crypto_tracked_user WHERE user_id=$1", userID).Scan(&ignore) + if errors.Is(err, sql.ErrNoRows) { return nil, nil } else if err != nil { return nil, err } - rows, err := store.DB.Query("SELECT device_id, identity_key, signing_key, trust, deleted, name FROM crypto_device WHERE user_id=$1 AND deleted=false", userID) + rows, err := store.DB.Query(ctx, "SELECT user_id, device_id, identity_key, signing_key, trust, deleted, name FROM crypto_device WHERE user_id=$1 AND deleted=false", userID) if err != nil { return nil, err } data := make(map[id.DeviceID]*id.Device) - for rows.Next() { - var identity id.Device - err := rows.Scan(&identity.DeviceID, &identity.IdentityKey, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name) - if err != nil { - return nil, err - } - identity.UserID = userID - data[identity.DeviceID] = &identity + err = dbutil.NewRowIter(rows, scanDevice).Iter(func(device *id.Device) (bool, error) { + data[device.DeviceID] = device + return true, nil + }) + if err != nil { + return nil, err } return data, nil } // GetDevice returns the device dentity for a given user and device ID. -func (store *SQLCryptoStore) GetDevice(userID id.UserID, deviceID id.DeviceID) (*id.Device, error) { - var identity id.Device - err := store.DB.QueryRow(` - SELECT identity_key, signing_key, trust, deleted, name +func (store *SQLCryptoStore) GetDevice(ctx context.Context, userID id.UserID, deviceID id.DeviceID) (*id.Device, error) { + return scanDevice(store.DB.QueryRow(ctx, ` + SELECT user_id, device_id, identity_key, signing_key, trust, deleted, name FROM crypto_device WHERE user_id=$1 AND device_id=$2`, userID, deviceID, - ).Scan(&identity.IdentityKey, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name) - if err != nil { - if err == sql.ErrNoRows { - return nil, nil - } - return nil, err - } - identity.UserID = userID - identity.DeviceID = deviceID - return &identity, nil + )) } // FindDeviceByKey finds a specific device by its sender key. -func (store *SQLCryptoStore) FindDeviceByKey(userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) { - var identity id.Device - err := store.DB.QueryRow(` - SELECT device_id, signing_key, trust, deleted, name +func (store *SQLCryptoStore) FindDeviceByKey(ctx context.Context, userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) { + return scanDevice(store.DB.QueryRow(ctx, ` + SELECT user_id, device_id, identity_key, signing_key, trust, deleted, name FROM crypto_device WHERE user_id=$1 AND identity_key=$2`, userID, identityKey, - ).Scan(&identity.DeviceID, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name) - if err != nil { - if err == sql.ErrNoRows { - return nil, nil - } - return nil, err - } - identity.UserID = userID - identity.IdentityKey = identityKey - return &identity, nil + )) } const deviceInsertQuery = ` @@ -693,106 +659,115 @@ ON CONFLICT (user_id, device_id) DO UPDATE var deviceMassInsertTemplate = strings.ReplaceAll(deviceInsertQuery, "($1, $2, $3, $4, $5, $6, $7)", "%s") // PutDevice stores a single device for a user, replacing it if it exists already. -func (store *SQLCryptoStore) PutDevice(userID id.UserID, device *id.Device) error { - _, err := store.DB.Exec(deviceInsertQuery, +func (store *SQLCryptoStore) PutDevice(ctx context.Context, userID id.UserID, device *id.Device) error { + _, err := store.DB.Exec(ctx, deviceInsertQuery, userID, device.DeviceID, device.IdentityKey, device.SigningKey, device.Trust, device.Deleted, device.Name) return err } +const trackedUserUpsertQuery = ` +INSERT INTO crypto_tracked_user (user_id, devices_outdated) +VALUES ($1, false) +ON CONFLICT (user_id) DO UPDATE + SET devices_outdated = EXCLUDED.devices_outdated +` + // PutDevices stores the device identity information for the given user ID. -func (store *SQLCryptoStore) PutDevices(userID id.UserID, devices map[id.DeviceID]*id.Device) error { - tx, err := store.DB.Begin() - if err != nil { - return err - } - - _, err = tx.Exec("INSERT INTO crypto_tracked_user (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID) - if err != nil { - return fmt.Errorf("failed to add user to tracked users list: %w", err) - } - - _, err = tx.Exec("UPDATE crypto_device SET deleted=true WHERE user_id=$1", userID) - if err != nil { - _ = tx.Rollback() - return fmt.Errorf("failed to delete old devices: %w", err) - } - if len(devices) == 0 { - err = tx.Commit() +func (store *SQLCryptoStore) PutDevices(ctx context.Context, userID id.UserID, devices map[id.DeviceID]*id.Device) error { + return store.DB.DoTxn(ctx, nil, func(ctx context.Context) error { + _, err := store.DB.Exec(ctx, trackedUserUpsertQuery, userID) if err != nil { - return fmt.Errorf("failed to commit changes (no devices added): %w", err) + return fmt.Errorf("failed to upsert user to tracked users list: %w", err) + } + + _, err = store.DB.Exec(ctx, "UPDATE crypto_device SET deleted=true WHERE user_id=$1", userID) + if err != nil { + return fmt.Errorf("failed to delete old devices: %w", err) + } + if len(devices) == 0 { + return nil + } + deviceBatchLen := 5 // how many devices will be inserted per query + deviceIDs := make([]id.DeviceID, 0, len(devices)) + for deviceID := range devices { + deviceIDs = append(deviceIDs, deviceID) + } + const valueStringFormat = "($1, $%d, $%d, $%d, $%d, $%d, $%d)" + for batchDeviceIdx := 0; batchDeviceIdx < len(deviceIDs); batchDeviceIdx += deviceBatchLen { + var batchDevices []id.DeviceID + if batchDeviceIdx+deviceBatchLen < len(deviceIDs) { + batchDevices = deviceIDs[batchDeviceIdx : batchDeviceIdx+deviceBatchLen] + } else { + batchDevices = deviceIDs[batchDeviceIdx:] + } + values := make([]interface{}, 1, len(devices)*6+1) + values[0] = userID + valueStrings := make([]string, 0, len(devices)) + i := 2 + for _, deviceID := range batchDevices { + identity := devices[deviceID] + values = append(values, deviceID, identity.IdentityKey, identity.SigningKey, identity.Trust, identity.Deleted, identity.Name) + valueStrings = append(valueStrings, fmt.Sprintf(valueStringFormat, i, i+1, i+2, i+3, i+4, i+5)) + i += 6 + } + valueString := strings.Join(valueStrings, ",") + _, err = store.DB.Exec(ctx, fmt.Sprintf(deviceMassInsertTemplate, valueString), values...) + if err != nil { + return fmt.Errorf("failed to insert new devices: %w", err) + } } return nil - } - deviceBatchLen := 5 // how many devices will be inserted per query - deviceIDs := make([]id.DeviceID, 0, len(devices)) - for deviceID := range devices { - deviceIDs = append(deviceIDs, deviceID) - } - const valueStringFormat = "($1, $%d, $%d, $%d, $%d, $%d, $%d)" - for batchDeviceIdx := 0; batchDeviceIdx < len(deviceIDs); batchDeviceIdx += deviceBatchLen { - var batchDevices []id.DeviceID - if batchDeviceIdx+deviceBatchLen < len(deviceIDs) { - batchDevices = deviceIDs[batchDeviceIdx : batchDeviceIdx+deviceBatchLen] - } else { - batchDevices = deviceIDs[batchDeviceIdx:] - } - values := make([]interface{}, 1, len(devices)*6+1) - values[0] = userID - valueStrings := make([]string, 0, len(devices)) - i := 2 - for _, deviceID := range batchDevices { - identity := devices[deviceID] - values = append(values, deviceID, identity.IdentityKey, identity.SigningKey, identity.Trust, identity.Deleted, identity.Name) - valueStrings = append(valueStrings, fmt.Sprintf(valueStringFormat, i, i+1, i+2, i+3, i+4, i+5)) - i += 6 - } - valueString := strings.Join(valueStrings, ",") - _, err = tx.Exec(fmt.Sprintf(deviceMassInsertTemplate, valueString), values...) - if err != nil { - _ = tx.Rollback() - return fmt.Errorf("failed to insert new devices: %w", err) - } - } - err = tx.Commit() - if err != nil { - return fmt.Errorf("failed to commit changes: %w", err) - } - return nil + }) } // FilterTrackedUsers finds all the user IDs out of the given ones for which the database contains identity information. -func (store *SQLCryptoStore) FilterTrackedUsers(users []id.UserID) ([]id.UserID, error) { +func (store *SQLCryptoStore) FilterTrackedUsers(ctx context.Context, users []id.UserID) ([]id.UserID, error) { var rows dbutil.Rows var err error if store.DB.Dialect == dbutil.Postgres && PostgresArrayWrapper != nil { - rows, err = store.DB.Query("SELECT user_id FROM crypto_tracked_user WHERE user_id = ANY($1)", PostgresArrayWrapper(users)) + rows, err = store.DB.Query(ctx, "SELECT user_id FROM crypto_tracked_user WHERE user_id = ANY($1)", PostgresArrayWrapper(users)) } else { queryString := make([]string, len(users)) params := make([]interface{}, len(users)) for i, user := range users { - queryString[i] = fmt.Sprintf("$%d", i+1) + queryString[i] = fmt.Sprintf("?%d", i+1) params[i] = user } - rows, err = store.DB.Query("SELECT user_id FROM crypto_tracked_user WHERE user_id IN ("+strings.Join(queryString, ",")+")", params...) + rows, err = store.DB.Query(ctx, "SELECT user_id FROM crypto_tracked_user WHERE user_id IN ("+strings.Join(queryString, ",")+")", params...) } if err != nil { return users, err } - var ptr int - for rows.Next() { - err = rows.Scan(&users[ptr]) - if err != nil { - return users, err - } else { - ptr++ + return dbutil.NewRowIter(rows, dbutil.ScanSingleColumn[id.UserID]).AsList() +} + +// MarkTrackedUsersOutdated flags that the device list for given users are outdated. +func (store *SQLCryptoStore) MarkTrackedUsersOutdated(ctx context.Context, users []id.UserID) error { + return store.DB.DoTxn(ctx, nil, func(ctx context.Context) error { + // TODO refactor to use a single query + for _, userID := range users { + _, err := store.DB.Exec(ctx, "UPDATE crypto_tracked_user SET devices_outdated = true WHERE user_id = $1", userID) + if err != nil { + return fmt.Errorf("failed to update user in the tracked users list: %w", err) + } } + + return nil + }) +} + +// GetOutdatedTrackerUsers gets all tracked users whose devices need to be updated. +func (store *SQLCryptoStore) GetOutdatedTrackedUsers(ctx context.Context) ([]id.UserID, error) { + rows, err := store.DB.Query(ctx, "SELECT user_id FROM crypto_tracked_user WHERE devices_outdated = TRUE") + if err != nil { + return nil, err } - return users[:ptr], nil + return dbutil.NewRowIter(rows, dbutil.ScanSingleColumn[id.UserID]).AsList() } // PutCrossSigningKey stores a cross-signing key of some user along with its usage. -func (store *SQLCryptoStore) PutCrossSigningKey(userID id.UserID, usage id.CrossSigningUsage, key id.Ed25519) error { - _, err := store.DB.Exec(` +func (store *SQLCryptoStore) PutCrossSigningKey(ctx context.Context, userID id.UserID, usage id.CrossSigningUsage, key id.Ed25519) error { + _, err := store.DB.Exec(ctx, ` INSERT INTO crypto_cross_signing_keys (user_id, usage, key, first_seen_key) VALUES ($1, $2, $3, $4) ON CONFLICT (user_id, usage) DO UPDATE SET key=excluded.key `, userID, usage, key, key) @@ -800,8 +775,8 @@ func (store *SQLCryptoStore) PutCrossSigningKey(userID id.UserID, usage id.Cross } // GetCrossSigningKeys retrieves a user's stored cross-signing keys. -func (store *SQLCryptoStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error) { - rows, err := store.DB.Query("SELECT usage, key, first_seen_key FROM crypto_cross_signing_keys WHERE user_id=$1", userID) +func (store *SQLCryptoStore) GetCrossSigningKeys(ctx context.Context, userID id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error) { + rows, err := store.DB.Query(ctx, "SELECT usage, key, first_seen_key FROM crypto_cross_signing_keys WHERE user_id=$1", userID) if err != nil { return nil, err } @@ -820,8 +795,8 @@ func (store *SQLCryptoStore) GetCrossSigningKeys(userID id.UserID) (map[id.Cross } // PutSignature stores a signature of a cross-signing or device key along with the signer's user ID and key. -func (store *SQLCryptoStore) PutSignature(signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519, signature string) error { - _, err := store.DB.Exec(` +func (store *SQLCryptoStore) PutSignature(ctx context.Context, signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519, signature string) error { + _, err := store.DB.Exec(ctx, ` INSERT INTO crypto_cross_signing_signatures (signed_user_id, signed_key, signer_user_id, signer_key, signature) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (signed_user_id, signed_key, signer_user_id, signer_key) DO UPDATE SET signature=excluded.signature `, signedUserID, signedKey, signerUserID, signerKey, signature) @@ -829,8 +804,8 @@ func (store *SQLCryptoStore) PutSignature(signedUserID id.UserID, signedKey id.E } // GetSignaturesForKeyBy retrieves the stored signatures for a given cross-signing or device key, by the given signer. -func (store *SQLCryptoStore) GetSignaturesForKeyBy(userID id.UserID, key id.Ed25519, signerID id.UserID) (map[id.Ed25519]string, error) { - rows, err := store.DB.Query("SELECT signer_key, signature FROM crypto_cross_signing_signatures WHERE signed_user_id=$1 AND signed_key=$2 AND signer_user_id=$3", userID, key, signerID) +func (store *SQLCryptoStore) GetSignaturesForKeyBy(ctx context.Context, userID id.UserID, key id.Ed25519, signerID id.UserID) (map[id.Ed25519]string, error) { + rows, err := store.DB.Query(ctx, "SELECT signer_key, signature FROM crypto_cross_signing_signatures WHERE signed_user_id=$1 AND signed_key=$2 AND signer_user_id=$3", userID, key, signerID) if err != nil { return nil, err } @@ -849,18 +824,18 @@ func (store *SQLCryptoStore) GetSignaturesForKeyBy(userID id.UserID, key id.Ed25 } // IsKeySignedBy returns whether a cross-signing or device key is signed by the given signer. -func (store *SQLCryptoStore) IsKeySignedBy(signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519) (isSigned bool, err error) { +func (store *SQLCryptoStore) IsKeySignedBy(ctx context.Context, signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519) (isSigned bool, err error) { q := `SELECT EXISTS( SELECT 1 FROM crypto_cross_signing_signatures WHERE signed_user_id=$1 AND signed_key=$2 AND signer_user_id=$3 AND signer_key=$4 )` - err = store.DB.QueryRow(q, signedUserID, signedKey, signerUserID, signerKey).Scan(&isSigned) + err = store.DB.QueryRow(ctx, q, signedUserID, signedKey, signerUserID, signerKey).Scan(&isSigned) return } // DropSignaturesByKey deletes the signatures made by the given user and key from the store. It returns the number of signatures deleted. -func (store *SQLCryptoStore) DropSignaturesByKey(userID id.UserID, key id.Ed25519) (int64, error) { - res, err := store.DB.Exec("DELETE FROM crypto_cross_signing_signatures WHERE signer_user_id=$1 AND signer_key=$2", userID, key) +func (store *SQLCryptoStore) DropSignaturesByKey(ctx context.Context, userID id.UserID, key id.Ed25519) (int64, error) { + res, err := store.DB.Exec(ctx, "DELETE FROM crypto_cross_signing_signatures WHERE signer_user_id=$1 AND signer_key=$2", userID, key) if err != nil { return 0, err } diff --git a/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/00-latest-revision.sql b/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/00-latest-revision.sql index bd8f794..90d7d31 100644 --- a/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/00-latest-revision.sql +++ b/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v10: Latest revision +-- v0 -> v11: Latest revision CREATE TABLE IF NOT EXISTS crypto_account ( account_id TEXT PRIMARY KEY, device_id TEXT NOT NULL, @@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS crypto_message_index ( ); CREATE TABLE IF NOT EXISTS crypto_tracked_user ( - user_id TEXT PRIMARY KEY + user_id TEXT PRIMARY KEY, + devices_outdated BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE IF NOT EXISTS crypto_device ( diff --git a/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/11-outdated-devices.sql b/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/11-outdated-devices.sql new file mode 100644 index 0000000..f0f0ba5 --- /dev/null +++ b/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/11-outdated-devices.sql @@ -0,0 +1,2 @@ +-- v11: Add devices_outdated field to crypto_tracked_user +ALTER TABLE crypto_tracked_user ADD COLUMN devices_outdated BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/upgrade.go b/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/upgrade.go index c9541b9..08c995d 100644 --- a/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/upgrade.go +++ b/vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/upgrade.go @@ -7,6 +7,7 @@ package sql_store_upgrade import ( + "context" "embed" "fmt" @@ -21,7 +22,7 @@ const VersionTableName = "crypto_version" var fs embed.FS func init() { - Table.Register(-1, 3, 0, "Unsupported version", false, func(tx dbutil.Execable, database *dbutil.Database) error { + Table.Register(-1, 3, 0, "Unsupported version", false, func(ctx context.Context, database *dbutil.Database) error { return fmt.Errorf("upgrading from versions 1 and 2 of the crypto store is no longer supported in mautrix-go v0.12+") }) Table.RegisterFS(fs) diff --git a/vendor/maunium.net/go/mautrix/crypto/ssss/client.go b/vendor/maunium.net/go/mautrix/crypto/ssss/client.go index b74deca..2dac30e 100644 --- a/vendor/maunium.net/go/mautrix/crypto/ssss/client.go +++ b/vendor/maunium.net/go/mautrix/crypto/ssss/client.go @@ -7,6 +7,7 @@ package ssss import ( + "context" "fmt" "maunium.net/go/mautrix" @@ -29,9 +30,9 @@ type DefaultSecretStorageKeyContent struct { } // GetDefaultKeyID retrieves the default key ID for this account from SSSS. -func (mach *Machine) GetDefaultKeyID() (string, error) { +func (mach *Machine) GetDefaultKeyID(ctx context.Context) (string, error) { var data DefaultSecretStorageKeyContent - err := mach.Client.GetAccountData(event.AccountDataSecretStorageDefaultKey.Type, &data) + err := mach.Client.GetAccountData(ctx, event.AccountDataSecretStorageDefaultKey.Type, &data) if err != nil { if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_NOT_FOUND" { return "", ErrNoDefaultKeyAccountDataEvent @@ -45,36 +46,36 @@ func (mach *Machine) GetDefaultKeyID() (string, error) { } // SetDefaultKeyID sets the default key ID for this account on the server. -func (mach *Machine) SetDefaultKeyID(keyID string) error { - return mach.Client.SetAccountData(event.AccountDataSecretStorageDefaultKey.Type, &DefaultSecretStorageKeyContent{keyID}) +func (mach *Machine) SetDefaultKeyID(ctx context.Context, keyID string) error { + return mach.Client.SetAccountData(ctx, event.AccountDataSecretStorageDefaultKey.Type, &DefaultSecretStorageKeyContent{keyID}) } // GetKeyData gets the details about the given key ID. -func (mach *Machine) GetKeyData(keyID string) (keyData *KeyMetadata, err error) { +func (mach *Machine) GetKeyData(ctx context.Context, keyID string) (keyData *KeyMetadata, err error) { keyData = &KeyMetadata{id: keyID} - err = mach.Client.GetAccountData(fmt.Sprintf("%s.%s", event.AccountDataSecretStorageKey.Type, keyID), keyData) + err = mach.Client.GetAccountData(ctx, fmt.Sprintf("%s.%s", event.AccountDataSecretStorageKey.Type, keyID), keyData) return } // SetKeyData stores SSSS key metadata on the server. -func (mach *Machine) SetKeyData(keyID string, keyData *KeyMetadata) error { - return mach.Client.SetAccountData(fmt.Sprintf("%s.%s", event.AccountDataSecretStorageKey.Type, keyID), keyData) +func (mach *Machine) SetKeyData(ctx context.Context, keyID string, keyData *KeyMetadata) error { + return mach.Client.SetAccountData(ctx, fmt.Sprintf("%s.%s", event.AccountDataSecretStorageKey.Type, keyID), keyData) } // GetDefaultKeyData gets the details about the default key ID (see GetDefaultKeyID). -func (mach *Machine) GetDefaultKeyData() (keyID string, keyData *KeyMetadata, err error) { - keyID, err = mach.GetDefaultKeyID() +func (mach *Machine) GetDefaultKeyData(ctx context.Context) (keyID string, keyData *KeyMetadata, err error) { + keyID, err = mach.GetDefaultKeyID(ctx) if err != nil { return } - keyData, err = mach.GetKeyData(keyID) + keyData, err = mach.GetKeyData(ctx, keyID) return } // GetDecryptedAccountData gets the account data event with the given event type and decrypts it using the given key. -func (mach *Machine) GetDecryptedAccountData(eventType event.Type, key *Key) ([]byte, error) { +func (mach *Machine) GetDecryptedAccountData(ctx context.Context, eventType event.Type, key *Key) ([]byte, error) { var encData EncryptedAccountDataEventContent - err := mach.Client.GetAccountData(eventType.Type, &encData) + err := mach.Client.GetAccountData(ctx, eventType.Type, &encData) if err != nil { return nil, err } @@ -82,7 +83,7 @@ func (mach *Machine) GetDecryptedAccountData(eventType event.Type, key *Key) ([] } // SetEncryptedAccountData encrypts the given data with the given keys and stores it on the server. -func (mach *Machine) SetEncryptedAccountData(eventType event.Type, data []byte, keys ...*Key) error { +func (mach *Machine) SetEncryptedAccountData(ctx context.Context, eventType event.Type, data []byte, keys ...*Key) error { if len(keys) == 0 { return ErrNoKeyGiven } @@ -90,17 +91,17 @@ func (mach *Machine) SetEncryptedAccountData(eventType event.Type, data []byte, for _, key := range keys { encrypted[key.ID] = key.Encrypt(eventType.Type, data) } - return mach.Client.SetAccountData(eventType.Type, &EncryptedAccountDataEventContent{Encrypted: encrypted}) + return mach.Client.SetAccountData(ctx, eventType.Type, &EncryptedAccountDataEventContent{Encrypted: encrypted}) } // GenerateAndUploadKey generates a new SSSS key and stores the metadata on the server. -func (mach *Machine) GenerateAndUploadKey(passphrase string) (key *Key, err error) { +func (mach *Machine) GenerateAndUploadKey(ctx context.Context, passphrase string) (key *Key, err error) { key, err = NewKey(passphrase) if err != nil { return nil, fmt.Errorf("failed to generate new key: %w", err) } - err = mach.SetKeyData(key.ID, key.Metadata) + err = mach.SetKeyData(ctx, key.ID, key.Metadata) if err != nil { err = fmt.Errorf("failed to upload key: %w", err) } diff --git a/vendor/maunium.net/go/mautrix/crypto/store.go b/vendor/maunium.net/go/mautrix/crypto/store.go index 99e464d..fb3d5b9 100644 --- a/vendor/maunium.net/go/mautrix/crypto/store.go +++ b/vendor/maunium.net/go/mautrix/crypto/store.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -26,64 +26,64 @@ var ErrGroupSessionWithheld error = &event.RoomKeyWithheldEventContent{} type Store interface { // Flush ensures that everything in the store is persisted to disk. // This doesn't have to do anything, e.g. for database-backed implementations that persist everything immediately. - Flush() error + Flush(context.Context) error // PutAccount updates the OlmAccount in the store. - PutAccount(*OlmAccount) error + PutAccount(context.Context, *OlmAccount) error // GetAccount returns the OlmAccount in the store that was previously inserted with PutAccount. - GetAccount() (*OlmAccount, error) + GetAccount(ctx context.Context) (*OlmAccount, error) // AddSession inserts an Olm session into the store. - AddSession(id.SenderKey, *OlmSession) error + AddSession(context.Context, id.SenderKey, *OlmSession) error // HasSession returns whether or not the store has an Olm session with the given sender key. - HasSession(id.SenderKey) bool + HasSession(context.Context, id.SenderKey) bool // GetSessions returns all Olm sessions in the store with the given sender key. - GetSessions(id.SenderKey) (OlmSessionList, error) + GetSessions(context.Context, id.SenderKey) (OlmSessionList, error) // GetLatestSession returns the session with the highest session ID (lexiographically sorting). // It's usually safe to return the most recently added session if sorting by session ID is too difficult. - GetLatestSession(id.SenderKey) (*OlmSession, error) + GetLatestSession(context.Context, id.SenderKey) (*OlmSession, error) // UpdateSession updates a session that has previously been inserted with AddSession. - UpdateSession(id.SenderKey, *OlmSession) error + UpdateSession(context.Context, id.SenderKey, *OlmSession) error // PutGroupSession inserts an inbound Megolm session into the store. If an earlier withhold event has been inserted // with PutWithheldGroupSession, this call should replace that. However, PutWithheldGroupSession must not replace // sessions inserted with this call. - PutGroupSession(id.RoomID, id.SenderKey, id.SessionID, *InboundGroupSession) error + PutGroupSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, *InboundGroupSession) error // GetGroupSession gets an inbound Megolm session from the store. If the group session has been withheld // (i.e. a room key withheld event has been saved with PutWithheldGroupSession), this should return the // ErrGroupSessionWithheld error. The caller may use GetWithheldGroupSession to find more details. - GetGroupSession(id.RoomID, id.SenderKey, id.SessionID) (*InboundGroupSession, error) + GetGroupSession(context.Context, id.RoomID, id.SenderKey, id.SessionID) (*InboundGroupSession, error) // RedactGroupSession removes the session data for the given inbound Megolm session from the store. - RedactGroupSession(id.RoomID, id.SenderKey, id.SessionID, string) error + RedactGroupSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, string) error // RedactGroupSessions removes the session data for all inbound Megolm sessions from a specific device and/or in a specific room. - RedactGroupSessions(id.RoomID, id.SenderKey, string) ([]id.SessionID, error) + RedactGroupSessions(context.Context, id.RoomID, id.SenderKey, string) ([]id.SessionID, error) // RedactExpiredGroupSessions removes the session data for all inbound Megolm sessions that have expired. - RedactExpiredGroupSessions() ([]id.SessionID, error) + RedactExpiredGroupSessions(context.Context) ([]id.SessionID, error) // RedactOutdatedGroupSessions removes the session data for all inbound Megolm sessions that are lacking the expiration metadata. - RedactOutdatedGroupSessions() ([]id.SessionID, error) + RedactOutdatedGroupSessions(context.Context) ([]id.SessionID, error) // PutWithheldGroupSession tells the store that a specific Megolm session was withheld. - PutWithheldGroupSession(event.RoomKeyWithheldEventContent) error + PutWithheldGroupSession(context.Context, event.RoomKeyWithheldEventContent) error // GetWithheldGroupSession gets the event content that was previously inserted with PutWithheldGroupSession. - GetWithheldGroupSession(id.RoomID, id.SenderKey, id.SessionID) (*event.RoomKeyWithheldEventContent, error) + GetWithheldGroupSession(context.Context, id.RoomID, id.SenderKey, id.SessionID) (*event.RoomKeyWithheldEventContent, error) // GetGroupSessionsForRoom gets all the inbound Megolm sessions for a specific room. This is used for creating key // export files. Unlike GetGroupSession, this should not return any errors about withheld keys. - GetGroupSessionsForRoom(id.RoomID) ([]*InboundGroupSession, error) + GetGroupSessionsForRoom(context.Context, id.RoomID) ([]*InboundGroupSession, error) // GetAllGroupSessions gets all the inbound Megolm sessions in the store. This is used for creating key export // files. Unlike GetGroupSession, this should not return any errors about withheld keys. - GetAllGroupSessions() ([]*InboundGroupSession, error) + GetAllGroupSessions(context.Context) ([]*InboundGroupSession, error) // AddOutboundGroupSession inserts the given outbound Megolm session into the store. // // The store should index inserted sessions by the RoomID field to support getting and removing sessions. // There will only be one outbound session per room ID at a time. - AddOutboundGroupSession(*OutboundGroupSession) error + AddOutboundGroupSession(context.Context, *OutboundGroupSession) error // UpdateOutboundGroupSession updates the given outbound Megolm session in the store. - UpdateOutboundGroupSession(*OutboundGroupSession) error + UpdateOutboundGroupSession(context.Context, *OutboundGroupSession) error // GetOutboundGroupSession gets the stored outbound Megolm session for the given room ID from the store. - GetOutboundGroupSession(id.RoomID) (*OutboundGroupSession, error) + GetOutboundGroupSession(context.Context, id.RoomID) (*OutboundGroupSession, error) // RemoveOutboundGroupSession removes the stored outbound Megolm session for the given room ID. - RemoveOutboundGroupSession(id.RoomID) error + RemoveOutboundGroupSession(context.Context, id.RoomID) error // ValidateMessageIndex validates that the given message details aren't from a replay attack. // @@ -96,29 +96,33 @@ type Store interface { ValidateMessageIndex(ctx context.Context, senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) (bool, error) // GetDevices returns a map from device ID to id.Device struct containing all devices of a given user. - GetDevices(id.UserID) (map[id.DeviceID]*id.Device, error) + GetDevices(context.Context, id.UserID) (map[id.DeviceID]*id.Device, error) // GetDevice returns a specific device of a given user. - GetDevice(id.UserID, id.DeviceID) (*id.Device, error) + GetDevice(context.Context, id.UserID, id.DeviceID) (*id.Device, error) // PutDevice stores a single device for a user, replacing it if it exists already. - PutDevice(id.UserID, *id.Device) error + PutDevice(context.Context, id.UserID, *id.Device) error // PutDevices overrides the stored device list for the given user with the given list. - PutDevices(id.UserID, map[id.DeviceID]*id.Device) error + PutDevices(context.Context, id.UserID, map[id.DeviceID]*id.Device) error // FindDeviceByKey finds a specific device by its identity key. - FindDeviceByKey(id.UserID, id.IdentityKey) (*id.Device, error) + FindDeviceByKey(context.Context, id.UserID, id.IdentityKey) (*id.Device, error) // FilterTrackedUsers returns a filtered version of the given list that only includes user IDs whose device lists // have been stored with PutDevices. A user is considered tracked even if the PutDevices list was empty. - FilterTrackedUsers([]id.UserID) ([]id.UserID, error) + FilterTrackedUsers(context.Context, []id.UserID) ([]id.UserID, error) + // MarkTrackedUsersOutdated flags that the device list for given users are outdated. + MarkTrackedUsersOutdated(context.Context, []id.UserID) error + // GetOutdatedTrackerUsers gets all tracked users whose devices need to be updated. + GetOutdatedTrackedUsers(context.Context) ([]id.UserID, error) // PutCrossSigningKey stores a cross-signing key of some user along with its usage. - PutCrossSigningKey(id.UserID, id.CrossSigningUsage, id.Ed25519) error + PutCrossSigningKey(context.Context, id.UserID, id.CrossSigningUsage, id.Ed25519) error // GetCrossSigningKeys retrieves a user's stored cross-signing keys. - GetCrossSigningKeys(id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error) + GetCrossSigningKeys(context.Context, id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error) // PutSignature stores a signature of a cross-signing or device key along with the signer's user ID and key. - PutSignature(signedUser id.UserID, signedKey id.Ed25519, signerUser id.UserID, signerKey id.Ed25519, signature string) error + PutSignature(ctx context.Context, signedUser id.UserID, signedKey id.Ed25519, signerUser id.UserID, signerKey id.Ed25519, signature string) error // IsKeySignedBy returns whether a cross-signing or device key is signed by the given signer. - IsKeySignedBy(userID id.UserID, key id.Ed25519, signedByUser id.UserID, signedByKey id.Ed25519) (bool, error) + IsKeySignedBy(ctx context.Context, userID id.UserID, key id.Ed25519, signedByUser id.UserID, signedByKey id.Ed25519) (bool, error) // DropSignaturesByKey deletes the signatures made by the given user and key from the store. It returns the number of signatures deleted. - DropSignaturesByKey(id.UserID, id.Ed25519) (int64, error) + DropSignaturesByKey(context.Context, id.UserID, id.Ed25519) (int64, error) } type messageIndexKey struct { @@ -148,6 +152,7 @@ type MemoryStore struct { Devices map[id.UserID]map[id.DeviceID]*id.Device CrossSigningKeys map[id.UserID]map[id.CrossSigningUsage]id.CrossSigningKey KeySignatures map[id.UserID]map[id.Ed25519]map[id.UserID]map[id.Ed25519]string + OutdatedUsers map[id.UserID]struct{} } var _ Store = (*MemoryStore)(nil) @@ -167,21 +172,22 @@ func NewMemoryStore(saveCallback func() error) *MemoryStore { Devices: make(map[id.UserID]map[id.DeviceID]*id.Device), CrossSigningKeys: make(map[id.UserID]map[id.CrossSigningUsage]id.CrossSigningKey), KeySignatures: make(map[id.UserID]map[id.Ed25519]map[id.UserID]map[id.Ed25519]string), + OutdatedUsers: make(map[id.UserID]struct{}), } } -func (gs *MemoryStore) Flush() error { +func (gs *MemoryStore) Flush(_ context.Context) error { gs.lock.Lock() err := gs.save() gs.lock.Unlock() return err } -func (gs *MemoryStore) GetAccount() (*OlmAccount, error) { +func (gs *MemoryStore) GetAccount(_ context.Context) (*OlmAccount, error) { return gs.Account, nil } -func (gs *MemoryStore) PutAccount(account *OlmAccount) error { +func (gs *MemoryStore) PutAccount(_ context.Context, account *OlmAccount) error { gs.lock.Lock() gs.Account = account err := gs.save() @@ -189,7 +195,7 @@ func (gs *MemoryStore) PutAccount(account *OlmAccount) error { return err } -func (gs *MemoryStore) GetSessions(senderKey id.SenderKey) (OlmSessionList, error) { +func (gs *MemoryStore) GetSessions(_ context.Context, senderKey id.SenderKey) (OlmSessionList, error) { gs.lock.Lock() sessions, ok := gs.Sessions[senderKey] if !ok { @@ -200,7 +206,7 @@ func (gs *MemoryStore) GetSessions(senderKey id.SenderKey) (OlmSessionList, erro return sessions, nil } -func (gs *MemoryStore) AddSession(senderKey id.SenderKey, session *OlmSession) error { +func (gs *MemoryStore) AddSession(_ context.Context, senderKey id.SenderKey, session *OlmSession) error { gs.lock.Lock() sessions, _ := gs.Sessions[senderKey] gs.Sessions[senderKey] = append(sessions, session) @@ -210,19 +216,19 @@ func (gs *MemoryStore) AddSession(senderKey id.SenderKey, session *OlmSession) e return err } -func (gs *MemoryStore) UpdateSession(_ id.SenderKey, _ *OlmSession) error { +func (gs *MemoryStore) UpdateSession(_ context.Context, _ id.SenderKey, _ *OlmSession) error { // we don't need to do anything here because the session is a pointer and already stored in our map return gs.save() } -func (gs *MemoryStore) HasSession(senderKey id.SenderKey) bool { +func (gs *MemoryStore) HasSession(_ context.Context, senderKey id.SenderKey) bool { gs.lock.RLock() sessions, ok := gs.Sessions[senderKey] gs.lock.RUnlock() return ok && len(sessions) > 0 && !sessions[0].Expired() } -func (gs *MemoryStore) GetLatestSession(senderKey id.SenderKey) (*OlmSession, error) { +func (gs *MemoryStore) GetLatestSession(_ context.Context, senderKey id.SenderKey) (*OlmSession, error) { gs.lock.RLock() sessions, ok := gs.Sessions[senderKey] gs.lock.RUnlock() @@ -246,7 +252,7 @@ func (gs *MemoryStore) getGroupSessions(roomID id.RoomID, senderKey id.SenderKey return sender } -func (gs *MemoryStore) PutGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, igs *InboundGroupSession) error { +func (gs *MemoryStore) PutGroupSession(_ context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, igs *InboundGroupSession) error { gs.lock.Lock() gs.getGroupSessions(roomID, senderKey)[sessionID] = igs err := gs.save() @@ -254,7 +260,7 @@ func (gs *MemoryStore) PutGroupSession(roomID id.RoomID, senderKey id.SenderKey, return err } -func (gs *MemoryStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*InboundGroupSession, error) { +func (gs *MemoryStore) GetGroupSession(_ context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*InboundGroupSession, error) { gs.lock.Lock() session, ok := gs.getGroupSessions(roomID, senderKey)[sessionID] if !ok { @@ -269,7 +275,7 @@ func (gs *MemoryStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, return session, nil } -func (gs *MemoryStore) RedactGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, reason string) error { +func (gs *MemoryStore) RedactGroupSession(_ context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, reason string) error { gs.lock.Lock() delete(gs.getGroupSessions(roomID, senderKey), sessionID) err := gs.save() @@ -277,7 +283,7 @@ func (gs *MemoryStore) RedactGroupSession(roomID id.RoomID, senderKey id.SenderK return err } -func (gs *MemoryStore) RedactGroupSessions(roomID id.RoomID, senderKey id.SenderKey, reason string) ([]id.SessionID, error) { +func (gs *MemoryStore) RedactGroupSessions(_ context.Context, roomID id.RoomID, senderKey id.SenderKey, reason string) ([]id.SessionID, error) { gs.lock.Lock() var sessionIDs []id.SessionID if roomID != "" && senderKey != "" { @@ -315,11 +321,11 @@ func (gs *MemoryStore) RedactGroupSessions(roomID id.RoomID, senderKey id.Sender return sessionIDs, err } -func (gs *MemoryStore) RedactExpiredGroupSessions() ([]id.SessionID, error) { +func (gs *MemoryStore) RedactExpiredGroupSessions(_ context.Context) ([]id.SessionID, error) { return nil, fmt.Errorf("not implemented") } -func (gs *MemoryStore) RedactOutdatedGroupSessions() ([]id.SessionID, error) { +func (gs *MemoryStore) RedactOutdatedGroupSessions(_ context.Context) ([]id.SessionID, error) { return nil, fmt.Errorf("not implemented") } @@ -337,7 +343,7 @@ func (gs *MemoryStore) getWithheldGroupSessions(roomID id.RoomID, senderKey id.S return sender } -func (gs *MemoryStore) PutWithheldGroupSession(content event.RoomKeyWithheldEventContent) error { +func (gs *MemoryStore) PutWithheldGroupSession(_ context.Context, content event.RoomKeyWithheldEventContent) error { gs.lock.Lock() gs.getWithheldGroupSessions(content.RoomID, content.SenderKey)[content.SessionID] = &content err := gs.save() @@ -345,7 +351,7 @@ func (gs *MemoryStore) PutWithheldGroupSession(content event.RoomKeyWithheldEven return err } -func (gs *MemoryStore) GetWithheldGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*event.RoomKeyWithheldEventContent, error) { +func (gs *MemoryStore) GetWithheldGroupSession(_ context.Context, roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*event.RoomKeyWithheldEventContent, error) { gs.lock.Lock() session, ok := gs.getWithheldGroupSessions(roomID, senderKey)[sessionID] gs.lock.Unlock() @@ -355,7 +361,7 @@ func (gs *MemoryStore) GetWithheldGroupSession(roomID id.RoomID, senderKey id.Se return session, nil } -func (gs *MemoryStore) GetGroupSessionsForRoom(roomID id.RoomID) ([]*InboundGroupSession, error) { +func (gs *MemoryStore) GetGroupSessionsForRoom(_ context.Context, roomID id.RoomID) ([]*InboundGroupSession, error) { gs.lock.Lock() defer gs.lock.Unlock() room, ok := gs.GroupSessions[roomID] @@ -371,7 +377,7 @@ func (gs *MemoryStore) GetGroupSessionsForRoom(roomID id.RoomID) ([]*InboundGrou return result, nil } -func (gs *MemoryStore) GetAllGroupSessions() ([]*InboundGroupSession, error) { +func (gs *MemoryStore) GetAllGroupSessions(_ context.Context) ([]*InboundGroupSession, error) { gs.lock.Lock() var result []*InboundGroupSession for _, room := range gs.GroupSessions { @@ -385,7 +391,7 @@ func (gs *MemoryStore) GetAllGroupSessions() ([]*InboundGroupSession, error) { return result, nil } -func (gs *MemoryStore) AddOutboundGroupSession(session *OutboundGroupSession) error { +func (gs *MemoryStore) AddOutboundGroupSession(_ context.Context, session *OutboundGroupSession) error { gs.lock.Lock() gs.OutGroupSessions[session.RoomID] = session err := gs.save() @@ -393,12 +399,12 @@ func (gs *MemoryStore) AddOutboundGroupSession(session *OutboundGroupSession) er return err } -func (gs *MemoryStore) UpdateOutboundGroupSession(_ *OutboundGroupSession) error { +func (gs *MemoryStore) UpdateOutboundGroupSession(_ context.Context, _ *OutboundGroupSession) error { // we don't need to do anything here because the session is a pointer and already stored in our map return gs.save() } -func (gs *MemoryStore) GetOutboundGroupSession(roomID id.RoomID) (*OutboundGroupSession, error) { +func (gs *MemoryStore) GetOutboundGroupSession(_ context.Context, roomID id.RoomID) (*OutboundGroupSession, error) { gs.lock.RLock() session, ok := gs.OutGroupSessions[roomID] gs.lock.RUnlock() @@ -408,7 +414,7 @@ func (gs *MemoryStore) GetOutboundGroupSession(roomID id.RoomID) (*OutboundGroup return session, nil } -func (gs *MemoryStore) RemoveOutboundGroupSession(roomID id.RoomID) error { +func (gs *MemoryStore) RemoveOutboundGroupSession(_ context.Context, roomID id.RoomID) error { gs.lock.Lock() session, ok := gs.OutGroupSessions[roomID] if !ok || session == nil { @@ -443,7 +449,7 @@ func (gs *MemoryStore) ValidateMessageIndex(_ context.Context, senderKey id.Send return true, nil } -func (gs *MemoryStore) GetDevices(userID id.UserID) (map[id.DeviceID]*id.Device, error) { +func (gs *MemoryStore) GetDevices(_ context.Context, userID id.UserID) (map[id.DeviceID]*id.Device, error) { gs.lock.RLock() devices, ok := gs.Devices[userID] if !ok { @@ -453,7 +459,7 @@ func (gs *MemoryStore) GetDevices(userID id.UserID) (map[id.DeviceID]*id.Device, return devices, nil } -func (gs *MemoryStore) GetDevice(userID id.UserID, deviceID id.DeviceID) (*id.Device, error) { +func (gs *MemoryStore) GetDevice(_ context.Context, userID id.UserID, deviceID id.DeviceID) (*id.Device, error) { gs.lock.RLock() defer gs.lock.RUnlock() devices, ok := gs.Devices[userID] @@ -467,7 +473,7 @@ func (gs *MemoryStore) GetDevice(userID id.UserID, deviceID id.DeviceID) (*id.De return device, nil } -func (gs *MemoryStore) FindDeviceByKey(userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) { +func (gs *MemoryStore) FindDeviceByKey(_ context.Context, userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) { gs.lock.RLock() defer gs.lock.RUnlock() devices, ok := gs.Devices[userID] @@ -482,7 +488,7 @@ func (gs *MemoryStore) FindDeviceByKey(userID id.UserID, identityKey id.Identity return nil, nil } -func (gs *MemoryStore) PutDevice(userID id.UserID, device *id.Device) error { +func (gs *MemoryStore) PutDevice(_ context.Context, userID id.UserID, device *id.Device) error { gs.lock.Lock() devices, ok := gs.Devices[userID] if !ok { @@ -495,15 +501,18 @@ func (gs *MemoryStore) PutDevice(userID id.UserID, device *id.Device) error { return err } -func (gs *MemoryStore) PutDevices(userID id.UserID, devices map[id.DeviceID]*id.Device) error { +func (gs *MemoryStore) PutDevices(_ context.Context, userID id.UserID, devices map[id.DeviceID]*id.Device) error { gs.lock.Lock() gs.Devices[userID] = devices err := gs.save() + if err == nil { + delete(gs.OutdatedUsers, userID) + } gs.lock.Unlock() return err } -func (gs *MemoryStore) FilterTrackedUsers(users []id.UserID) ([]id.UserID, error) { +func (gs *MemoryStore) FilterTrackedUsers(_ context.Context, users []id.UserID) ([]id.UserID, error) { gs.lock.RLock() var ptr int for _, userID := range users { @@ -517,7 +526,28 @@ func (gs *MemoryStore) FilterTrackedUsers(users []id.UserID) ([]id.UserID, error return users[:ptr], nil } -func (gs *MemoryStore) PutCrossSigningKey(userID id.UserID, usage id.CrossSigningUsage, key id.Ed25519) error { +func (gs *MemoryStore) MarkTrackedUsersOutdated(_ context.Context, users []id.UserID) error { + gs.lock.Lock() + for _, userID := range users { + if _, ok := gs.Devices[userID]; ok { + gs.OutdatedUsers[userID] = struct{}{} + } + } + gs.lock.Unlock() + return nil +} + +func (gs *MemoryStore) GetOutdatedTrackedUsers(_ context.Context) ([]id.UserID, error) { + gs.lock.RLock() + users := make([]id.UserID, 0, len(gs.OutdatedUsers)) + for userID := range gs.OutdatedUsers { + users = append(users, userID) + } + gs.lock.RUnlock() + return users, nil +} + +func (gs *MemoryStore) PutCrossSigningKey(_ context.Context, userID id.UserID, usage id.CrossSigningUsage, key id.Ed25519) error { gs.lock.RLock() userKeys, ok := gs.CrossSigningKeys[userID] if !ok { @@ -539,7 +569,7 @@ func (gs *MemoryStore) PutCrossSigningKey(userID id.UserID, usage id.CrossSignin return err } -func (gs *MemoryStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error) { +func (gs *MemoryStore) GetCrossSigningKeys(_ context.Context, userID id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error) { gs.lock.RLock() defer gs.lock.RUnlock() keys, ok := gs.CrossSigningKeys[userID] @@ -549,7 +579,7 @@ func (gs *MemoryStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSignin return keys, nil } -func (gs *MemoryStore) PutSignature(signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519, signature string) error { +func (gs *MemoryStore) PutSignature(_ context.Context, signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519, signature string) error { gs.lock.RLock() signedUserSigs, ok := gs.KeySignatures[signedUserID] if !ok { @@ -572,7 +602,7 @@ func (gs *MemoryStore) PutSignature(signedUserID id.UserID, signedKey id.Ed25519 return err } -func (gs *MemoryStore) GetSignaturesForKeyBy(userID id.UserID, key id.Ed25519, signerID id.UserID) (map[id.Ed25519]string, error) { +func (gs *MemoryStore) GetSignaturesForKeyBy(_ context.Context, userID id.UserID, key id.Ed25519, signerID id.UserID) (map[id.Ed25519]string, error) { gs.lock.RLock() defer gs.lock.RUnlock() userKeys, ok := gs.KeySignatures[userID] @@ -590,8 +620,8 @@ func (gs *MemoryStore) GetSignaturesForKeyBy(userID id.UserID, key id.Ed25519, s return sigsBySigner, nil } -func (gs *MemoryStore) IsKeySignedBy(userID id.UserID, key id.Ed25519, signerID id.UserID, signerKey id.Ed25519) (bool, error) { - sigs, err := gs.GetSignaturesForKeyBy(userID, key, signerID) +func (gs *MemoryStore) IsKeySignedBy(ctx context.Context, userID id.UserID, key id.Ed25519, signerID id.UserID, signerKey id.Ed25519) (bool, error) { + sigs, err := gs.GetSignaturesForKeyBy(ctx, userID, key, signerID) if err != nil { return false, err } @@ -599,7 +629,7 @@ func (gs *MemoryStore) IsKeySignedBy(userID id.UserID, key id.Ed25519, signerID return ok, nil } -func (gs *MemoryStore) DropSignaturesByKey(userID id.UserID, key id.Ed25519) (int64, error) { +func (gs *MemoryStore) DropSignaturesByKey(_ context.Context, userID id.UserID, key id.Ed25519) (int64, error) { var count int64 gs.lock.RLock() for _, userSigs := range gs.KeySignatures { diff --git a/vendor/maunium.net/go/mautrix/crypto/utils/utils.go b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go index 414d83b..382db02 100644 --- a/vendor/maunium.net/go/mautrix/crypto/utils/utils.go +++ b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go @@ -10,10 +10,10 @@ import ( "crypto/aes" "crypto/cipher" "crypto/hmac" + "crypto/rand" "crypto/sha256" "crypto/sha512" "encoding/base64" - "math/rand" "strings" "go.mau.fi/util/base58" diff --git a/vendor/maunium.net/go/mautrix/crypto/verification.go b/vendor/maunium.net/go/mautrix/crypto/verification.go index 4925fed..31608bf 100644 --- a/vendor/maunium.net/go/mautrix/crypto/verification.go +++ b/vendor/maunium.net/go/mautrix/crypto/verification.go @@ -54,8 +54,8 @@ const ( ) // sendToOneDevice sends a to-device event to a single device. -func (mach *OlmMachine) sendToOneDevice(userID id.UserID, deviceID id.DeviceID, eventType event.Type, content interface{}) error { - _, err := mach.Client.SendToDevice(eventType, &mautrix.ReqSendToDevice{ +func (mach *OlmMachine) sendToOneDevice(ctx context.Context, userID id.UserID, deviceID id.DeviceID, eventType event.Type, content interface{}) error { + _, err := mach.Client.SendToDevice(ctx, eventType, &mautrix.ReqSendToDevice{ Messages: map[id.UserID]map[id.DeviceID]*event.Content{ userID: { deviceID: { @@ -118,19 +118,19 @@ type verificationState struct { } // getTransactionState retrieves the given transaction's state, or cancels the transaction if it cannot be found or there is a mismatch. -func (mach *OlmMachine) getTransactionState(transactionID string, userID id.UserID) (*verificationState, error) { +func (mach *OlmMachine) getTransactionState(ctx context.Context, transactionID string, userID id.UserID) (*verificationState, error) { verStateInterface, ok := mach.keyVerificationTransactionState.Load(userID.String() + ":" + transactionID) if !ok { - _ = mach.SendSASVerificationCancel(userID, id.DeviceID("*"), transactionID, "Unknown transaction: "+transactionID, event.VerificationCancelUnknownTransaction) + _ = mach.SendSASVerificationCancel(ctx, userID, id.DeviceID("*"), transactionID, "Unknown transaction: "+transactionID, event.VerificationCancelUnknownTransaction) return nil, ErrUnknownTransaction } verState := verStateInterface.(*verificationState) if verState.otherDevice.UserID != userID { reason := fmt.Sprintf("Unknown user for transaction %v: %v", transactionID, userID) if verState.inRoomID == "" { - _ = mach.SendSASVerificationCancel(userID, id.DeviceID("*"), transactionID, reason, event.VerificationCancelUserMismatch) + _ = mach.SendSASVerificationCancel(ctx, userID, id.DeviceID("*"), transactionID, reason, event.VerificationCancelUserMismatch) } else { - _ = mach.SendInRoomSASVerificationCancel(verState.inRoomID, userID, transactionID, reason, event.VerificationCancelUserMismatch) + _ = mach.SendInRoomSASVerificationCancel(ctx, verState.inRoomID, userID, transactionID, reason, event.VerificationCancelUserMismatch) } mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) return nil, fmt.Errorf("%w %s: %s", ErrUnknownUserForTransaction, transactionID, userID) @@ -140,9 +140,9 @@ func (mach *OlmMachine) getTransactionState(transactionID string, userID id.User // handleVerificationStart handles an incoming m.key.verification.start message. // It initializes the state for this SAS verification process and stores it. -func (mach *OlmMachine) handleVerificationStart(userID id.UserID, content *event.VerificationStartEventContent, transactionID string, timeout time.Duration, inRoomID id.RoomID) { +func (mach *OlmMachine) handleVerificationStart(ctx context.Context, userID id.UserID, content *event.VerificationStartEventContent, transactionID string, timeout time.Duration, inRoomID id.RoomID) { mach.Log.Debug().Msgf("Received verification start from %v", content.FromDevice) - otherDevice, err := mach.GetOrFetchDevice(context.TODO(), userID, content.FromDevice) + otherDevice, err := mach.GetOrFetchDevice(ctx, userID, content.FromDevice) if err != nil { mach.Log.Error().Msgf("Could not find device %v of user %v", content.FromDevice, userID) return @@ -150,9 +150,9 @@ func (mach *OlmMachine) handleVerificationStart(userID id.UserID, content *event warnAndCancel := func(logReason, cancelReason string) { mach.Log.Warn().Msgf("Canceling verification transaction %v as it %s", transactionID, logReason) if inRoomID == "" { - _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, cancelReason, event.VerificationCancelUnknownMethod) + _ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, cancelReason, event.VerificationCancelUnknownMethod) } else { - _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, cancelReason, event.VerificationCancelUnknownMethod) + _ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, cancelReason, event.VerificationCancelUnknownMethod) } } switch { @@ -168,21 +168,21 @@ func (mach *OlmMachine) handleVerificationStart(userID id.UserID, content *event case !content.SupportsSASMethod(event.SASDecimal): warnAndCancel("does not support decimal SAS", "Decimal SAS method must be supported") default: - mach.actuallyStartVerification(userID, content, otherDevice, transactionID, timeout, inRoomID) + mach.actuallyStartVerification(ctx, userID, content, otherDevice, transactionID, timeout, inRoomID) } } -func (mach *OlmMachine) actuallyStartVerification(userID id.UserID, content *event.VerificationStartEventContent, otherDevice *id.Device, transactionID string, timeout time.Duration, inRoomID id.RoomID) { +func (mach *OlmMachine) actuallyStartVerification(ctx context.Context, userID id.UserID, content *event.VerificationStartEventContent, otherDevice *id.Device, transactionID string, timeout time.Duration, inRoomID id.RoomID) { if inRoomID != "" && transactionID != "" { - verState, err := mach.getTransactionState(transactionID, userID) + verState, err := mach.getTransactionState(ctx, transactionID, userID) if err != nil { mach.Log.Error().Msgf("Failed to get transaction state for in-room verification %s start: %v", transactionID, err) - _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Internal state error in gomuks :(", "net.maunium.internal_error") + _ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Internal state error in gomuks :(", "net.maunium.internal_error") return } - mach.timeoutAfter(verState, transactionID, timeout) + mach.timeoutAfter(ctx, verState, transactionID, timeout) sasMethods := commonSASMethods(verState.hooks, content.ShortAuthenticationString) - err = mach.SendInRoomSASVerificationAccept(inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods) + err = mach.SendInRoomSASVerificationAccept(ctx, inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods) if err != nil { mach.Log.Error().Msgf("Error accepting in-room SAS verification: %v", err) } @@ -196,9 +196,9 @@ func (mach *OlmMachine) actuallyStartVerification(userID id.UserID, content *eve if len(sasMethods) == 0 { mach.Log.Error().Msgf("No common SAS methods: %v", content.ShortAuthenticationString) if inRoomID == "" { - _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod) + _ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod) } else { - _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod) + _ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod) } return } @@ -221,20 +221,20 @@ func (mach *OlmMachine) actuallyStartVerification(userID id.UserID, content *eve // transaction already exists mach.Log.Error().Msgf("Transaction %v already exists, canceling", transactionID) if inRoomID == "" { - _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage) + _ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage) } else { - _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage) + _ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage) } return } - mach.timeoutAfter(verState, transactionID, timeout) + mach.timeoutAfter(ctx, verState, transactionID, timeout) var err error if inRoomID == "" { - err = mach.SendSASVerificationAccept(userID, content, verState.sas.GetPubkey(), sasMethods) + err = mach.SendSASVerificationAccept(ctx, userID, content, verState.sas.GetPubkey(), sasMethods) } else { - err = mach.SendInRoomSASVerificationAccept(inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods) + err = mach.SendInRoomSASVerificationAccept(ctx, inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods) } if err != nil { mach.Log.Error().Msgf("Error accepting SAS verification: %v", err) @@ -243,9 +243,9 @@ func (mach *OlmMachine) actuallyStartVerification(userID id.UserID, content *eve mach.Log.Debug().Msgf("Not accepting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) var err error if inRoomID == "" { - err = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser) + err = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser) } else { - err = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser) + err = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser) } if err != nil { mach.Log.Error().Msgf("Error canceling SAS verification: %v", err) @@ -255,8 +255,8 @@ func (mach *OlmMachine) actuallyStartVerification(userID id.UserID, content *eve } } -func (mach *OlmMachine) timeoutAfter(verState *verificationState, transactionID string, timeout time.Duration) { - timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), timeout) +func (mach *OlmMachine) timeoutAfter(ctx context.Context, verState *verificationState, transactionID string, timeout time.Duration) { + timeoutCtx, timeoutCancel := context.WithTimeout(ctx, timeout) verState.extendTimeout = timeoutCancel go func() { mapKey := verState.otherDevice.UserID.String() + ":" + transactionID @@ -272,7 +272,7 @@ func (mach *OlmMachine) timeoutAfter(verState *verificationState, transactionID if timeoutCtx.Err() == context.DeadlineExceeded { // if deadline exceeded cancel due to timeout mach.keyVerificationTransactionState.Delete(mapKey) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Timed out", event.VerificationCancelByTimeout) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Timed out", event.VerificationCancelByTimeout) mach.Log.Warn().Msgf("Verification transaction %v is canceled due to timing out", transactionID) verState.lock.Unlock() return @@ -288,9 +288,9 @@ func (mach *OlmMachine) timeoutAfter(verState *verificationState, transactionID // handleVerificationAccept handles an incoming m.key.verification.accept message. // It continues the SAS verification process by sending the SAS key message to the other device. -func (mach *OlmMachine) handleVerificationAccept(userID id.UserID, content *event.VerificationAcceptEventContent, transactionID string) { +func (mach *OlmMachine) handleVerificationAccept(ctx context.Context, userID id.UserID, content *event.VerificationAcceptEventContent, transactionID string) { mach.Log.Debug().Msgf("Received verification accept for transaction %v", transactionID) - verState, err := mach.getTransactionState(transactionID, userID) + verState, err := mach.getTransactionState(ctx, transactionID, userID) if err != nil { mach.Log.Error().Msgf("Error getting transaction state: %v", err) return @@ -303,7 +303,7 @@ func (mach *OlmMachine) handleVerificationAccept(userID id.UserID, content *even // unexpected accept at this point mach.Log.Warn().Msgf("Unexpected verification accept message for transaction %v", transactionID) mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected accept message", event.VerificationCancelUnexpectedMessage) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Unexpected accept message", event.VerificationCancelUnexpectedMessage) return } @@ -315,7 +315,7 @@ func (mach *OlmMachine) handleVerificationAccept(userID id.UserID, content *even mach.Log.Warn().Msgf("Canceling verification transaction %v due to unknown parameter", transactionID) mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Verification uses unknown method", event.VerificationCancelUnknownMethod) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Verification uses unknown method", event.VerificationCancelUnknownMethod) return } @@ -325,9 +325,9 @@ func (mach *OlmMachine) handleVerificationAccept(userID id.UserID, content *even verState.verificationStarted = true if verState.inRoomID == "" { - err = mach.SendSASVerificationKey(userID, verState.otherDevice.DeviceID, transactionID, string(key)) + err = mach.SendSASVerificationKey(ctx, userID, verState.otherDevice.DeviceID, transactionID, string(key)) } else { - err = mach.SendInRoomSASVerificationKey(verState.inRoomID, userID, transactionID, string(key)) + err = mach.SendInRoomSASVerificationKey(ctx, verState.inRoomID, userID, transactionID, string(key)) } if err != nil { mach.Log.Error().Msgf("Error sending SAS key to other device: %v", err) @@ -337,9 +337,9 @@ func (mach *OlmMachine) handleVerificationAccept(userID id.UserID, content *even // handleVerificationKey handles an incoming m.key.verification.key message. // It stores the other device's public key in order to acquire the SAS shared secret. -func (mach *OlmMachine) handleVerificationKey(userID id.UserID, content *event.VerificationKeyEventContent, transactionID string) { +func (mach *OlmMachine) handleVerificationKey(ctx context.Context, userID id.UserID, content *event.VerificationKeyEventContent, transactionID string) { mach.Log.Debug().Msgf("Got verification key for transaction %v: %v", transactionID, content.Key) - verState, err := mach.getTransactionState(transactionID, userID) + verState, err := mach.getTransactionState(ctx, transactionID, userID) if err != nil { mach.Log.Error().Msgf("Error getting transaction state: %v", err) return @@ -354,7 +354,7 @@ func (mach *OlmMachine) handleVerificationKey(userID id.UserID, content *event.V // unexpected key at this point mach.Log.Warn().Msgf("Unexpected verification key message for transaction %v", transactionID) mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected key message", event.VerificationCancelUnexpectedMessage) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Unexpected key message", event.VerificationCancelUnexpectedMessage) return } @@ -372,7 +372,7 @@ func (mach *OlmMachine) handleVerificationKey(userID id.UserID, content *event.V if expectedCommitment != verState.commitment { mach.Log.Warn().Msgf("Canceling verification transaction %v due to commitment mismatch", transactionID) mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Commitment mismatch", event.VerificationCancelCommitmentMismatch) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Commitment mismatch", event.VerificationCancelCommitmentMismatch) return } } else { @@ -380,9 +380,9 @@ func (mach *OlmMachine) handleVerificationKey(userID id.UserID, content *event.V key := verState.sas.GetPubkey() if verState.inRoomID == "" { - err = mach.SendSASVerificationKey(userID, device.DeviceID, transactionID, string(key)) + err = mach.SendSASVerificationKey(ctx, userID, device.DeviceID, transactionID, string(key)) } else { - err = mach.SendInRoomSASVerificationKey(verState.inRoomID, userID, transactionID, string(key)) + err = mach.SendInRoomSASVerificationKey(ctx, verState.inRoomID, userID, transactionID, string(key)) } if err != nil { mach.Log.Error().Msgf("Error sending SAS key to other device: %v", err) @@ -419,13 +419,13 @@ func (mach *OlmMachine) handleVerificationKey(userID id.UserID, content *event.V mach.Log.Debug().Msgf("Generated SAS (%v): %v", sasMethod.Type(), sas) go func() { result := verState.hooks.VerifySASMatch(device, sas) - mach.sasCompared(result, transactionID, verState) + mach.sasCompared(ctx, result, transactionID, verState) }() } // sasCompared is called asynchronously. It waits for the SAS to be compared for the verification to proceed. // If the SAS match, then our MAC is sent out. Otherwise the transaction is canceled. -func (mach *OlmMachine) sasCompared(didMatch bool, transactionID string, verState *verificationState) { +func (mach *OlmMachine) sasCompared(ctx context.Context, didMatch bool, transactionID string, verState *verificationState) { verState.lock.Lock() defer verState.lock.Unlock() verState.extendTimeout() @@ -433,9 +433,9 @@ func (mach *OlmMachine) sasCompared(didMatch bool, transactionID string, verStat verState.sasMatched <- true var err error if verState.inRoomID == "" { - err = mach.SendSASVerificationMAC(verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas) + err = mach.SendSASVerificationMAC(ctx, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas) } else { - err = mach.SendInRoomSASVerificationMAC(verState.inRoomID, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas) + err = mach.SendInRoomSASVerificationMAC(ctx, verState.inRoomID, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas) } if err != nil { mach.Log.Error().Msgf("Error sending verification MAC to other device: %v", err) @@ -447,9 +447,9 @@ func (mach *OlmMachine) sasCompared(didMatch bool, transactionID string, verStat // handleVerificationMAC handles an incoming m.key.verification.mac message. // It verifies the other device's MAC and if the MAC is valid it marks the device as trusted. -func (mach *OlmMachine) handleVerificationMAC(userID id.UserID, content *event.VerificationMacEventContent, transactionID string) { +func (mach *OlmMachine) handleVerificationMAC(ctx context.Context, userID id.UserID, content *event.VerificationMacEventContent, transactionID string) { mach.Log.Debug().Msgf("Got MAC for verification %v: %v, MAC for keys: %v", transactionID, content.Mac, content.Keys) - verState, err := mach.getTransactionState(transactionID, userID) + verState, err := mach.getTransactionState(ctx, transactionID, userID) if err != nil { mach.Log.Error().Msgf("Error getting transaction state: %v", err) return @@ -466,7 +466,7 @@ func (mach *OlmMachine) handleVerificationMAC(userID id.UserID, content *event.V if !verState.verificationStarted || !verState.keyReceived { // unexpected MAC at this point mach.Log.Warn().Msgf("Unexpected MAC message for transaction %v", transactionID) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected MAC message", event.VerificationCancelUnexpectedMessage) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Unexpected MAC message", event.VerificationCancelUnexpectedMessage) return } @@ -478,7 +478,7 @@ func (mach *OlmMachine) handleVerificationMAC(userID id.UserID, content *event.V if !matched { mach.Log.Warn().Msgf("SAS do not match! Canceling transaction %v", transactionID) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "SAS do not match", event.VerificationCancelSASMismatch) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "SAS do not match", event.VerificationCancelSASMismatch) return } @@ -494,38 +494,38 @@ func (mach *OlmMachine) handleVerificationMAC(userID id.UserID, content *event.V mach.Log.Debug().Msgf("Expected %s keys MAC, got %s", expectedKeysMAC, content.Keys) if content.Keys != expectedKeysMAC { mach.Log.Warn().Msgf("Canceling verification transaction %v due to mismatched keys MAC", transactionID) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Mismatched keys MACs", event.VerificationCancelKeyMismatch) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Mismatched keys MACs", event.VerificationCancelKeyMismatch) return } mach.Log.Debug().Msgf("Expected %s PK MAC, got %s", expectedPKMAC, content.Mac[keyID]) if content.Mac[keyID] != expectedPKMAC { mach.Log.Warn().Msgf("Canceling verification transaction %v due to mismatched PK MAC", transactionID) - _ = mach.callbackAndCancelSASVerification(verState, transactionID, "Mismatched PK MACs", event.VerificationCancelKeyMismatch) + _ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Mismatched PK MACs", event.VerificationCancelKeyMismatch) return } // we can finally trust this device device.Trust = id.TrustStateVerified - err = mach.CryptoStore.PutDevice(device.UserID, device) + err = mach.CryptoStore.PutDevice(ctx, device.UserID, device) if err != nil { mach.Log.Warn().Msgf("Failed to put device after verifying: %v", err) } if mach.CrossSigningKeys != nil { if device.UserID == mach.Client.UserID { - err := mach.SignOwnDevice(device) + err := mach.SignOwnDevice(ctx, device) if err != nil { mach.Log.Error().Msgf("Failed to cross-sign own device %s: %v", device.DeviceID, err) } else { mach.Log.Debug().Msgf("Cross-signed own device %v after SAS verification", device.DeviceID) } } else { - masterKey, err := mach.fetchMasterKey(device, content, verState, transactionID) + masterKey, err := mach.fetchMasterKey(ctx, device, content, verState, transactionID) if err != nil { mach.Log.Warn().Msgf("Failed to fetch %s's master key: %v", device.UserID, err) } else { - if err := mach.SignUser(device.UserID, masterKey); err != nil { + if err := mach.SignUser(ctx, device.UserID, masterKey); err != nil { mach.Log.Error().Msgf("Failed to cross-sign master key of %s: %v", device.UserID, err) } else { mach.Log.Debug().Msgf("Cross-signed master key of %v after SAS verification", device.UserID) @@ -559,9 +559,9 @@ func (mach *OlmMachine) handleVerificationCancel(userID id.UserID, content *even } // handleVerificationRequest handles an incoming m.key.verification.request message. -func (mach *OlmMachine) handleVerificationRequest(userID id.UserID, content *event.VerificationRequestEventContent, transactionID string, inRoomID id.RoomID) { +func (mach *OlmMachine) handleVerificationRequest(ctx context.Context, userID id.UserID, content *event.VerificationRequestEventContent, transactionID string, inRoomID id.RoomID) { mach.Log.Debug().Msgf("Received verification request from %v", content.FromDevice) - otherDevice, err := mach.GetOrFetchDevice(context.TODO(), userID, content.FromDevice) + otherDevice, err := mach.GetOrFetchDevice(ctx, userID, content.FromDevice) if err != nil { mach.Log.Error().Msgf("Could not find device %v of user %v", content.FromDevice, userID) return @@ -569,9 +569,9 @@ func (mach *OlmMachine) handleVerificationRequest(userID id.UserID, content *eve if !content.SupportsVerificationMethod(event.VerificationMethodSAS) { mach.Log.Warn().Msgf("Canceling verification transaction %v as SAS is not supported", transactionID) if inRoomID == "" { - _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod) + _ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod) } else { - _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod) + _ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod) } return } @@ -579,14 +579,14 @@ func (mach *OlmMachine) handleVerificationRequest(userID id.UserID, content *eve if resp == AcceptRequest { mach.Log.Debug().Msgf("Accepting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) if inRoomID == "" { - _, err = mach.NewSASVerificationWith(otherDevice, hooks, transactionID, mach.DefaultSASTimeout) + _, err = mach.NewSASVerificationWith(ctx, otherDevice, hooks, transactionID, mach.DefaultSASTimeout) } else { - if err := mach.SendInRoomSASVerificationReady(inRoomID, transactionID); err != nil { + if err := mach.SendInRoomSASVerificationReady(ctx, inRoomID, transactionID); err != nil { mach.Log.Error().Msgf("Error sending in-room SAS verification ready: %v", err) } if mach.Client.UserID < otherDevice.UserID { // up to us to send the start message - _, err = mach.newInRoomSASVerificationWithInner(inRoomID, otherDevice, hooks, transactionID, mach.DefaultSASTimeout) + _, err = mach.newInRoomSASVerificationWithInner(ctx, inRoomID, otherDevice, hooks, transactionID, mach.DefaultSASTimeout) } } if err != nil { @@ -595,9 +595,9 @@ func (mach *OlmMachine) handleVerificationRequest(userID id.UserID, content *eve } else if resp == RejectRequest { mach.Log.Debug().Msgf("Rejecting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) if inRoomID == "" { - _ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser) + _ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser) } else { - _ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser) + _ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser) } } else { mach.Log.Debug().Msgf("Ignoring SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID) @@ -606,14 +606,14 @@ func (mach *OlmMachine) handleVerificationRequest(userID id.UserID, content *eve // NewSimpleSASVerificationWith starts the SAS verification process with another device with a default timeout, // a generated transaction ID and support for both emoji and decimal SAS methods. -func (mach *OlmMachine) NewSimpleSASVerificationWith(device *id.Device, hooks VerificationHooks) (string, error) { - return mach.NewSASVerificationWith(device, hooks, "", mach.DefaultSASTimeout) +func (mach *OlmMachine) NewSimpleSASVerificationWith(ctx context.Context, device *id.Device, hooks VerificationHooks) (string, error) { + return mach.NewSASVerificationWith(ctx, device, hooks, "", mach.DefaultSASTimeout) } // NewSASVerificationWith starts the SAS verification process with another device. // If the other device accepts the verification transaction, the methods in `hooks` will be used to verify the SAS match and to complete the transaction.. // If the transaction ID is empty, a new one is generated. -func (mach *OlmMachine) NewSASVerificationWith(device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) { +func (mach *OlmMachine) NewSASVerificationWith(ctx context.Context, device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) { if transactionID == "" { transactionID = strconv.Itoa(rand.Int()) } @@ -631,7 +631,7 @@ func (mach *OlmMachine) NewSASVerificationWith(device *id.Device, hooks Verifica verState.lock.Lock() defer verState.lock.Unlock() - startEvent, err := mach.SendSASVerificationStart(device.UserID, device.DeviceID, transactionID, hooks.VerificationMethods()) + startEvent, err := mach.SendSASVerificationStart(ctx, device.UserID, device.DeviceID, transactionID, hooks.VerificationMethods()) if err != nil { return "", err } @@ -651,13 +651,13 @@ func (mach *OlmMachine) NewSASVerificationWith(device *id.Device, hooks Verifica return "", ErrTransactionAlreadyExists } - mach.timeoutAfter(verState, transactionID, timeout) + mach.timeoutAfter(ctx, verState, transactionID, timeout) return transactionID, nil } // CancelSASVerification is used by the user to cancel a SAS verification process with the given reason. -func (mach *OlmMachine) CancelSASVerification(userID id.UserID, transactionID, reason string) error { +func (mach *OlmMachine) CancelSASVerification(ctx context.Context, userID id.UserID, transactionID, reason string) error { mapKey := userID.String() + ":" + transactionID verStateInterface, ok := mach.keyVerificationTransactionState.Load(mapKey) if !ok { @@ -668,21 +668,21 @@ func (mach *OlmMachine) CancelSASVerification(userID id.UserID, transactionID, r defer verState.lock.Unlock() mach.Log.Trace().Msgf("User canceled verification transaction %v with reason: %v", transactionID, reason) mach.keyVerificationTransactionState.Delete(mapKey) - return mach.callbackAndCancelSASVerification(verState, transactionID, reason, event.VerificationCancelByUser) + return mach.callbackAndCancelSASVerification(ctx, verState, transactionID, reason, event.VerificationCancelByUser) } // SendSASVerificationCancel is used to manually send a SAS cancel message process with the given reason and cancellation code. -func (mach *OlmMachine) SendSASVerificationCancel(userID id.UserID, deviceID id.DeviceID, transactionID string, reason string, code event.VerificationCancelCode) error { +func (mach *OlmMachine) SendSASVerificationCancel(ctx context.Context, userID id.UserID, deviceID id.DeviceID, transactionID string, reason string, code event.VerificationCancelCode) error { content := &event.VerificationCancelEventContent{ TransactionID: transactionID, Reason: reason, Code: code, } - return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationCancel, content) + return mach.sendToOneDevice(ctx, userID, deviceID, event.ToDeviceVerificationCancel, content) } // SendSASVerificationStart is used to manually send the SAS verification start message to another device. -func (mach *OlmMachine) SendSASVerificationStart(toUserID id.UserID, toDeviceID id.DeviceID, transactionID string, methods []VerificationMethod) (*event.VerificationStartEventContent, error) { +func (mach *OlmMachine) SendSASVerificationStart(ctx context.Context, toUserID id.UserID, toDeviceID id.DeviceID, transactionID string, methods []VerificationMethod) (*event.VerificationStartEventContent, error) { sasMethods := make([]event.SASMethod, len(methods)) for i, method := range methods { sasMethods[i] = method.Type() @@ -696,14 +696,14 @@ func (mach *OlmMachine) SendSASVerificationStart(toUserID id.UserID, toDeviceID MessageAuthenticationCodes: []event.MACMethod{event.HKDFHMACSHA256}, ShortAuthenticationString: sasMethods, } - return content, mach.sendToOneDevice(toUserID, toDeviceID, event.ToDeviceVerificationStart, content) + return content, mach.sendToOneDevice(ctx, toUserID, toDeviceID, event.ToDeviceVerificationStart, content) } // SendSASVerificationAccept is used to manually send an accept for a SAS verification process from a received m.key.verification.start event. -func (mach *OlmMachine) SendSASVerificationAccept(fromUser id.UserID, startEvent *event.VerificationStartEventContent, publicKey []byte, methods []VerificationMethod) error { +func (mach *OlmMachine) SendSASVerificationAccept(ctx context.Context, fromUser id.UserID, startEvent *event.VerificationStartEventContent, publicKey []byte, methods []VerificationMethod) error { if startEvent.Method != event.VerificationMethodSAS { reason := "Unknown verification method: " + string(startEvent.Method) - if err := mach.SendSASVerificationCancel(fromUser, startEvent.FromDevice, startEvent.TransactionID, reason, event.VerificationCancelUnknownMethod); err != nil { + if err := mach.SendSASVerificationCancel(ctx, fromUser, startEvent.FromDevice, startEvent.TransactionID, reason, event.VerificationCancelUnknownMethod); err != nil { return err } return ErrUnknownVerificationMethod @@ -730,25 +730,25 @@ func (mach *OlmMachine) SendSASVerificationAccept(fromUser id.UserID, startEvent ShortAuthenticationString: sasMethods, Commitment: hash, } - return mach.sendToOneDevice(fromUser, startEvent.FromDevice, event.ToDeviceVerificationAccept, content) + return mach.sendToOneDevice(ctx, fromUser, startEvent.FromDevice, event.ToDeviceVerificationAccept, content) } -func (mach *OlmMachine) callbackAndCancelSASVerification(verState *verificationState, transactionID, reason string, code event.VerificationCancelCode) error { +func (mach *OlmMachine) callbackAndCancelSASVerification(ctx context.Context, verState *verificationState, transactionID, reason string, code event.VerificationCancelCode) error { go verState.hooks.OnCancel(true, reason, code) - return mach.SendSASVerificationCancel(verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, reason, code) + return mach.SendSASVerificationCancel(ctx, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, reason, code) } // SendSASVerificationKey sends the ephemeral public key for a device to the partner device. -func (mach *OlmMachine) SendSASVerificationKey(userID id.UserID, deviceID id.DeviceID, transactionID string, key string) error { +func (mach *OlmMachine) SendSASVerificationKey(ctx context.Context, userID id.UserID, deviceID id.DeviceID, transactionID string, key string) error { content := &event.VerificationKeyEventContent{ TransactionID: transactionID, Key: key, } - return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationKey, content) + return mach.sendToOneDevice(ctx, userID, deviceID, event.ToDeviceVerificationKey, content) } // SendSASVerificationMAC is use the MAC of a device's key to the partner device. -func (mach *OlmMachine) SendSASVerificationMAC(userID id.UserID, deviceID id.DeviceID, transactionID string, sas *olm.SAS) error { +func (mach *OlmMachine) SendSASVerificationMAC(ctx context.Context, userID id.UserID, deviceID id.DeviceID, transactionID string, sas *olm.SAS) error { keyID := id.NewKeyID(id.KeyAlgorithmEd25519, mach.Client.DeviceID.String()) signingKey := mach.account.SigningKey() @@ -784,7 +784,7 @@ func (mach *OlmMachine) SendSASVerificationMAC(userID id.UserID, deviceID id.Dev Mac: macMap, } - return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationMAC, content) + return mach.sendToOneDevice(ctx, userID, deviceID, event.ToDeviceVerificationMAC, content) } func commonSASMethods(hooks VerificationHooks, otherDeviceMethods []event.SASMethod) []VerificationMethod { diff --git a/vendor/maunium.net/go/mautrix/crypto/verification_in_room.go b/vendor/maunium.net/go/mautrix/crypto/verification_in_room.go index cc9b921..240c52b 100644 --- a/vendor/maunium.net/go/mautrix/crypto/verification_in_room.go +++ b/vendor/maunium.net/go/mautrix/crypto/verification_in_room.go @@ -38,6 +38,7 @@ func (mach *OlmMachine) ProcessInRoomVerification(evt *event.Event) error { return ErrNoRelatesTo } + ctx := context.TODO() switch content := evt.Content.Parsed.(type) { case *event.MessageEventContent: if content.MsgType == event.MsgVerificationRequest { @@ -54,18 +55,18 @@ func (mach *OlmMachine) ProcessInRoomVerification(evt *event.Event) error { Timestamp: evt.Timestamp, TransactionID: evt.ID.String(), } - mach.handleVerificationRequest(evt.Sender, newContent, evt.ID.String(), evt.RoomID) + mach.handleVerificationRequest(ctx, evt.Sender, newContent, evt.ID.String(), evt.RoomID) } case *event.VerificationStartEventContent: - mach.handleVerificationStart(evt.Sender, content, content.RelatesTo.EventID.String(), 10*time.Minute, evt.RoomID) + mach.handleVerificationStart(ctx, evt.Sender, content, content.RelatesTo.EventID.String(), 10*time.Minute, evt.RoomID) case *event.VerificationReadyEventContent: - mach.handleInRoomVerificationReady(evt.Sender, evt.RoomID, content, content.RelatesTo.EventID.String()) + mach.handleInRoomVerificationReady(ctx, evt.Sender, evt.RoomID, content, content.RelatesTo.EventID.String()) case *event.VerificationAcceptEventContent: - mach.handleVerificationAccept(evt.Sender, content, content.RelatesTo.EventID.String()) + mach.handleVerificationAccept(ctx, evt.Sender, content, content.RelatesTo.EventID.String()) case *event.VerificationKeyEventContent: - mach.handleVerificationKey(evt.Sender, content, content.RelatesTo.EventID.String()) + mach.handleVerificationKey(ctx, evt.Sender, content, content.RelatesTo.EventID.String()) case *event.VerificationMacEventContent: - mach.handleVerificationMAC(evt.Sender, content, content.RelatesTo.EventID.String()) + mach.handleVerificationMAC(ctx, evt.Sender, content, content.RelatesTo.EventID.String()) case *event.VerificationCancelEventContent: mach.handleVerificationCancel(evt.Sender, content, content.RelatesTo.EventID.String()) } @@ -73,7 +74,7 @@ func (mach *OlmMachine) ProcessInRoomVerification(evt *event.Event) error { } // SendInRoomSASVerificationCancel is used to manually send an in-room SAS cancel message process with the given reason and cancellation code. -func (mach *OlmMachine) SendInRoomSASVerificationCancel(roomID id.RoomID, userID id.UserID, transactionID string, reason string, code event.VerificationCancelCode) error { +func (mach *OlmMachine) SendInRoomSASVerificationCancel(ctx context.Context, roomID id.RoomID, userID id.UserID, transactionID string, reason string, code event.VerificationCancelCode) error { content := &event.VerificationCancelEventContent{ RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)}, Reason: reason, @@ -81,16 +82,16 @@ func (mach *OlmMachine) SendInRoomSASVerificationCancel(roomID id.RoomID, userID To: userID, } - encrypted, err := mach.EncryptMegolmEvent(context.TODO(), roomID, event.InRoomVerificationCancel, content) + encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationCancel, content) if err != nil { return err } - _, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted) + _, err = mach.Client.SendMessageEvent(ctx, roomID, event.EventEncrypted, encrypted) return err } // SendInRoomSASVerificationRequest is used to manually send an in-room SAS verification request message to another user. -func (mach *OlmMachine) SendInRoomSASVerificationRequest(roomID id.RoomID, toUserID id.UserID, methods []VerificationMethod) (string, error) { +func (mach *OlmMachine) SendInRoomSASVerificationRequest(ctx context.Context, roomID id.RoomID, toUserID id.UserID, methods []VerificationMethod) (string, error) { content := &event.MessageEventContent{ MsgType: event.MsgVerificationRequest, FromDevice: mach.Client.DeviceID, @@ -98,11 +99,11 @@ func (mach *OlmMachine) SendInRoomSASVerificationRequest(roomID id.RoomID, toUse To: toUserID, } - encrypted, err := mach.EncryptMegolmEvent(context.TODO(), roomID, event.EventMessage, content) + encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.EventMessage, content) if err != nil { return "", err } - resp, err := mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted) + resp, err := mach.Client.SendMessageEvent(ctx, roomID, event.EventEncrypted, encrypted) if err != nil { return "", err } @@ -110,23 +111,23 @@ func (mach *OlmMachine) SendInRoomSASVerificationRequest(roomID id.RoomID, toUse } // SendInRoomSASVerificationReady is used to manually send an in-room SAS verification ready message to another user. -func (mach *OlmMachine) SendInRoomSASVerificationReady(roomID id.RoomID, transactionID string) error { +func (mach *OlmMachine) SendInRoomSASVerificationReady(ctx context.Context, roomID id.RoomID, transactionID string) error { content := &event.VerificationReadyEventContent{ FromDevice: mach.Client.DeviceID, Methods: []event.VerificationMethod{event.VerificationMethodSAS}, RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)}, } - encrypted, err := mach.EncryptMegolmEvent(context.TODO(), roomID, event.InRoomVerificationReady, content) + encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationReady, content) if err != nil { return err } - _, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted) + _, err = mach.Client.SendMessageEvent(ctx, roomID, event.EventEncrypted, encrypted) return err } // SendInRoomSASVerificationStart is used to manually send the in-room SAS verification start message to another user. -func (mach *OlmMachine) SendInRoomSASVerificationStart(roomID id.RoomID, toUserID id.UserID, transactionID string, methods []VerificationMethod) (*event.VerificationStartEventContent, error) { +func (mach *OlmMachine) SendInRoomSASVerificationStart(ctx context.Context, roomID id.RoomID, toUserID id.UserID, transactionID string, methods []VerificationMethod) (*event.VerificationStartEventContent, error) { sasMethods := make([]event.SASMethod, len(methods)) for i, method := range methods { sasMethods[i] = method.Type() @@ -142,19 +143,19 @@ func (mach *OlmMachine) SendInRoomSASVerificationStart(roomID id.RoomID, toUserI To: toUserID, } - encrypted, err := mach.EncryptMegolmEvent(context.TODO(), roomID, event.InRoomVerificationStart, content) + encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationStart, content) if err != nil { return nil, err } - _, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted) + _, err = mach.Client.SendMessageEvent(ctx, roomID, event.EventEncrypted, encrypted) return content, err } // SendInRoomSASVerificationAccept is used to manually send an accept for an in-room SAS verification process from a received m.key.verification.start event. -func (mach *OlmMachine) SendInRoomSASVerificationAccept(roomID id.RoomID, fromUser id.UserID, startEvent *event.VerificationStartEventContent, transactionID string, publicKey []byte, methods []VerificationMethod) error { +func (mach *OlmMachine) SendInRoomSASVerificationAccept(ctx context.Context, roomID id.RoomID, fromUser id.UserID, startEvent *event.VerificationStartEventContent, transactionID string, publicKey []byte, methods []VerificationMethod) error { if startEvent.Method != event.VerificationMethodSAS { reason := "Unknown verification method: " + string(startEvent.Method) - if err := mach.SendInRoomSASVerificationCancel(roomID, fromUser, transactionID, reason, event.VerificationCancelUnknownMethod); err != nil { + if err := mach.SendInRoomSASVerificationCancel(ctx, roomID, fromUser, transactionID, reason, event.VerificationCancelUnknownMethod); err != nil { return err } return ErrUnknownVerificationMethod @@ -183,32 +184,32 @@ func (mach *OlmMachine) SendInRoomSASVerificationAccept(roomID id.RoomID, fromUs To: fromUser, } - encrypted, err := mach.EncryptMegolmEvent(context.TODO(), roomID, event.InRoomVerificationAccept, content) + encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationAccept, content) if err != nil { return err } - _, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted) + _, err = mach.Client.SendMessageEvent(ctx, roomID, event.EventEncrypted, encrypted) return err } // SendInRoomSASVerificationKey sends the ephemeral public key for a device to the partner device for an in-room verification. -func (mach *OlmMachine) SendInRoomSASVerificationKey(roomID id.RoomID, userID id.UserID, transactionID string, key string) error { +func (mach *OlmMachine) SendInRoomSASVerificationKey(ctx context.Context, roomID id.RoomID, userID id.UserID, transactionID string, key string) error { content := &event.VerificationKeyEventContent{ RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)}, Key: key, To: userID, } - encrypted, err := mach.EncryptMegolmEvent(context.TODO(), roomID, event.InRoomVerificationKey, content) + encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationKey, content) if err != nil { return err } - _, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted) + _, err = mach.Client.SendMessageEvent(ctx, roomID, event.EventEncrypted, encrypted) return err } // SendInRoomSASVerificationMAC sends the MAC of a device's key to the partner device for an in-room verification. -func (mach *OlmMachine) SendInRoomSASVerificationMAC(roomID id.RoomID, userID id.UserID, deviceID id.DeviceID, transactionID string, sas *olm.SAS) error { +func (mach *OlmMachine) SendInRoomSASVerificationMAC(ctx context.Context, roomID id.RoomID, userID id.UserID, deviceID id.DeviceID, transactionID string, sas *olm.SAS) error { keyID := id.NewKeyID(id.KeyAlgorithmEd25519, mach.Client.DeviceID.String()) signingKey := mach.account.SigningKey() @@ -245,28 +246,28 @@ func (mach *OlmMachine) SendInRoomSASVerificationMAC(roomID id.RoomID, userID id To: userID, } - encrypted, err := mach.EncryptMegolmEvent(context.TODO(), roomID, event.InRoomVerificationMAC, content) + encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationMAC, content) if err != nil { return err } - _, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted) + _, err = mach.Client.SendMessageEvent(ctx, roomID, event.EventEncrypted, encrypted) return err } // NewInRoomSASVerificationWith starts the in-room SAS verification process with another user in the given room. // It returns the generated transaction ID. -func (mach *OlmMachine) NewInRoomSASVerificationWith(inRoomID id.RoomID, userID id.UserID, hooks VerificationHooks, timeout time.Duration) (string, error) { - return mach.newInRoomSASVerificationWithInner(inRoomID, &id.Device{UserID: userID}, hooks, "", timeout) +func (mach *OlmMachine) NewInRoomSASVerificationWith(ctx context.Context, inRoomID id.RoomID, userID id.UserID, hooks VerificationHooks, timeout time.Duration) (string, error) { + return mach.newInRoomSASVerificationWithInner(ctx, inRoomID, &id.Device{UserID: userID}, hooks, "", timeout) } -func (mach *OlmMachine) newInRoomSASVerificationWithInner(inRoomID id.RoomID, device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) { +func (mach *OlmMachine) newInRoomSASVerificationWithInner(ctx context.Context, inRoomID id.RoomID, device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) { mach.Log.Debug().Msgf("Starting new in-room verification transaction user %v", device.UserID) request := transactionID == "" if request { var err error // get new transaction ID from the request message event ID - transactionID, err = mach.SendInRoomSASVerificationRequest(inRoomID, device.UserID, hooks.VerificationMethods()) + transactionID, err = mach.SendInRoomSASVerificationRequest(ctx, inRoomID, device.UserID, hooks.VerificationMethods()) if err != nil { return "", err } @@ -286,7 +287,7 @@ func (mach *OlmMachine) newInRoomSASVerificationWithInner(inRoomID id.RoomID, de if !request { // start in-room verification - startEvent, err := mach.SendInRoomSASVerificationStart(inRoomID, device.UserID, transactionID, hooks.VerificationMethods()) + startEvent, err := mach.SendInRoomSASVerificationStart(ctx, inRoomID, device.UserID, transactionID, hooks.VerificationMethods()) if err != nil { return "", err } @@ -305,19 +306,19 @@ func (mach *OlmMachine) newInRoomSASVerificationWithInner(inRoomID id.RoomID, de mach.keyVerificationTransactionState.Store(device.UserID.String()+":"+transactionID, verState) - mach.timeoutAfter(verState, transactionID, timeout) + mach.timeoutAfter(ctx, verState, transactionID, timeout) return transactionID, nil } -func (mach *OlmMachine) handleInRoomVerificationReady(userID id.UserID, roomID id.RoomID, content *event.VerificationReadyEventContent, transactionID string) { - device, err := mach.GetOrFetchDevice(context.TODO(), userID, content.FromDevice) +func (mach *OlmMachine) handleInRoomVerificationReady(ctx context.Context, userID id.UserID, roomID id.RoomID, content *event.VerificationReadyEventContent, transactionID string) { + device, err := mach.GetOrFetchDevice(ctx, userID, content.FromDevice) if err != nil { mach.Log.Error().Msgf("Error fetching device %v of user %v: %v", content.FromDevice, userID, err) return } - verState, err := mach.getTransactionState(transactionID, userID) + verState, err := mach.getTransactionState(ctx, transactionID, userID) if err != nil { mach.Log.Error().Msgf("Error getting transaction state: %v", err) return @@ -327,7 +328,7 @@ func (mach *OlmMachine) handleInRoomVerificationReady(userID id.UserID, roomID i if mach.Client.UserID < userID { // up to us to send the start message verState.lock.Lock() - mach.newInRoomSASVerificationWithInner(roomID, device, verState.hooks, transactionID, 10*time.Minute) + mach.newInRoomSASVerificationWithInner(ctx, roomID, device, verState.hooks, transactionID, 10*time.Minute) verState.lock.Unlock() } } diff --git a/vendor/maunium.net/go/mautrix/event/events.go b/vendor/maunium.net/go/mautrix/event/events.go index 5761122..f7b4d4d 100644 --- a/vendor/maunium.net/go/mautrix/event/events.go +++ b/vendor/maunium.net/go/mautrix/event/events.go @@ -105,6 +105,8 @@ func (evt *Event) MarshalJSON() ([]byte, error) { } type MautrixInfo struct { + EventSource Source + TrustState id.TrustState ForwardedKeys bool WasEncrypted bool diff --git a/vendor/maunium.net/go/mautrix/event/eventsource.go b/vendor/maunium.net/go/mautrix/event/eventsource.go new file mode 100644 index 0000000..86c1ceb --- /dev/null +++ b/vendor/maunium.net/go/mautrix/event/eventsource.go @@ -0,0 +1,72 @@ +// Copyright (c) 2024 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package event + +import ( + "fmt" +) + +// Source represents the part of the sync response that an event came from. +type Source int + +const ( + SourcePresence Source = 1 << iota + SourceJoin + SourceInvite + SourceLeave + SourceAccountData + SourceTimeline + SourceState + SourceEphemeral + SourceToDevice + SourceDecrypted +) + +const primaryTypes = SourcePresence | SourceAccountData | SourceToDevice | SourceTimeline | SourceState +const roomSections = SourceJoin | SourceInvite | SourceLeave +const roomableTypes = SourceAccountData | SourceTimeline | SourceState +const encryptableTypes = roomableTypes | SourceToDevice + +func (es Source) String() string { + var typeName string + switch es & primaryTypes { + case SourcePresence: + typeName = "presence" + case SourceAccountData: + typeName = "account data" + case SourceToDevice: + typeName = "to-device" + case SourceTimeline: + typeName = "timeline" + case SourceState: + typeName = "state" + default: + return fmt.Sprintf("unknown (%d)", es) + } + if es&roomableTypes != 0 { + switch es & roomSections { + case SourceJoin: + typeName = "joined room " + typeName + case SourceInvite: + typeName = "invited room " + typeName + case SourceLeave: + typeName = "left room " + typeName + default: + return fmt.Sprintf("unknown (%s+%d)", typeName, es) + } + es &^= roomSections + } + if es&encryptableTypes != 0 && es&SourceDecrypted != 0 { + typeName += " (decrypted)" + es &^= SourceDecrypted + } + es &^= primaryTypes + if es != 0 { + return fmt.Sprintf("unknown (%s+%d)", typeName, es) + } + return typeName +} diff --git a/vendor/maunium.net/go/mautrix/event/message.go b/vendor/maunium.net/go/mautrix/event/message.go index 009709a..6512f9b 100644 --- a/vendor/maunium.net/go/mautrix/event/message.go +++ b/vendor/maunium.net/go/mautrix/event/message.go @@ -199,10 +199,14 @@ type FileInfo struct { ThumbnailInfo *FileInfo `json:"thumbnail_info,omitempty"` ThumbnailURL id.ContentURIString `json:"thumbnail_url,omitempty"` ThumbnailFile *EncryptedFileInfo `json:"thumbnail_file,omitempty"` - Width int `json:"-"` - Height int `json:"-"` - Duration int `json:"-"` - Size int `json:"-"` + + Blurhash string `json:"blurhash,omitempty"` + AnoaBlurhash string `json:"xyz.amorgan.blurhash,omitempty"` + + Width int `json:"-"` + Height int `json:"-"` + Duration int `json:"-"` + Size int `json:"-"` } type serializableFileInfo struct { @@ -211,6 +215,9 @@ type serializableFileInfo struct { ThumbnailURL id.ContentURIString `json:"thumbnail_url,omitempty"` ThumbnailFile *EncryptedFileInfo `json:"thumbnail_file,omitempty"` + Blurhash string `json:"blurhash,omitempty"` + AnoaBlurhash string `json:"xyz.amorgan.blurhash,omitempty"` + Width json.Number `json:"w,omitempty"` Height json.Number `json:"h,omitempty"` Duration json.Number `json:"duration,omitempty"` @@ -226,6 +233,9 @@ func (sfi *serializableFileInfo) CopyFrom(fileInfo *FileInfo) *serializableFileI ThumbnailURL: fileInfo.ThumbnailURL, ThumbnailInfo: (&serializableFileInfo{}).CopyFrom(fileInfo.ThumbnailInfo), ThumbnailFile: fileInfo.ThumbnailFile, + + Blurhash: fileInfo.Blurhash, + AnoaBlurhash: fileInfo.AnoaBlurhash, } if fileInfo.Width > 0 { sfi.Width = json.Number(strconv.Itoa(fileInfo.Width)) @@ -252,6 +262,8 @@ func (sfi *serializableFileInfo) CopyTo(fileInfo *FileInfo) { MimeType: sfi.MimeType, ThumbnailURL: sfi.ThumbnailURL, ThumbnailFile: sfi.ThumbnailFile, + Blurhash: sfi.Blurhash, + AnoaBlurhash: sfi.AnoaBlurhash, } if sfi.ThumbnailInfo != nil { fileInfo.ThumbnailInfo = &FileInfo{} diff --git a/vendor/maunium.net/go/mautrix/requests.go b/vendor/maunium.net/go/mautrix/requests.go index 985c833..6e00346 100644 --- a/vendor/maunium.net/go/mautrix/requests.go +++ b/vendor/maunium.net/go/mautrix/requests.go @@ -287,9 +287,7 @@ type Signatures map[id.UserID]map[id.KeyID]string type ReqQueryKeys struct { DeviceKeys DeviceKeysRequest `json:"device_keys"` - - Timeout int64 `json:"timeout,omitempty"` - Token string `json:"token,omitempty"` + Timeout int64 `json:"timeout,omitempty"` } type DeviceKeysRequest map[id.UserID]DeviceIDList diff --git a/vendor/maunium.net/go/mautrix/sqlstatestore/statestore.go b/vendor/maunium.net/go/mautrix/sqlstatestore/statestore.go index 531b71e..cd94215 100644 --- a/vendor/maunium.net/go/mautrix/sqlstatestore/statestore.go +++ b/vendor/maunium.net/go/mautrix/sqlstatestore/statestore.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,6 +7,7 @@ package sqlstatestore import ( + "context" "database/sql" "embed" "encoding/json" @@ -15,6 +16,7 @@ import ( "strconv" "strings" + "github.com/rs/zerolog" "go.mau.fi/util/dbutil" "maunium.net/go/mautrix/event" @@ -44,26 +46,28 @@ func NewSQLStateStore(db *dbutil.Database, log dbutil.DatabaseLogger, isBridge b } } -func (store *SQLStateStore) IsRegistered(userID id.UserID) bool { +func (store *SQLStateStore) IsRegistered(ctx context.Context, userID id.UserID) (bool, error) { var isRegistered bool err := store. - QueryRow("SELECT EXISTS(SELECT 1 FROM mx_registrations WHERE user_id=$1)", userID). + QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM mx_registrations WHERE user_id=$1)", userID). Scan(&isRegistered) - if err != nil { - store.Log.Warn("Failed to scan registration existence for %s: %v", userID, err) + if errors.Is(err, sql.ErrNoRows) { + err = nil } - return isRegistered + return isRegistered, err } -func (store *SQLStateStore) MarkRegistered(userID id.UserID) { - _, err := store.Exec("INSERT INTO mx_registrations (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID) - if err != nil { - store.Log.Warn("Failed to mark %s as registered: %v", userID, err) - } +func (store *SQLStateStore) MarkRegistered(ctx context.Context, userID id.UserID) error { + _, err := store.Exec(ctx, "INSERT INTO mx_registrations (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID) + return err } -func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID, memberships ...event.Membership) map[id.UserID]*event.MemberEventContent { - members := make(map[id.UserID]*event.MemberEventContent) +type Member struct { + id.UserID + event.MemberEventContent +} + +func (store *SQLStateStore) GetRoomMembers(ctx context.Context, roomID id.RoomID, memberships ...event.Membership) (map[id.UserID]*event.MemberEventContent, error) { args := make([]any, len(memberships)+1) args[0] = roomID query := "SELECT user_id, membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1" @@ -75,25 +79,26 @@ func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID, memberships ...even } query = fmt.Sprintf("%s AND membership IN (%s)", query, strings.Join(placeholders, ",")) } - rows, err := store.Query(query, args...) + rows, err := store.Query(ctx, query, args...) if err != nil { - return members + return nil, err } - var userID id.UserID - var member event.MemberEventContent - for rows.Next() { - err = rows.Scan(&userID, &member.Membership, &member.Displayname, &member.AvatarURL) - if err != nil { - store.Log.Warn("Failed to scan member in %s: %v", roomID, err) - } else { - members[userID] = &member - } - } - return members + members := make(map[id.UserID]*event.MemberEventContent) + return members, dbutil.NewRowIter(rows, func(row dbutil.Scannable) (ret Member, err error) { + err = row.Scan(&ret.UserID, &ret.Membership, &ret.Displayname, &ret.AvatarURL) + return + }).Iter(func(m Member) (bool, error) { + members[m.UserID] = &m.MemberEventContent + return true, nil + }) } -func (store *SQLStateStore) GetRoomJoinedOrInvitedMembers(roomID id.RoomID) (members []id.UserID, err error) { - memberMap := store.GetRoomMembers(roomID, event.MembershipJoin, event.MembershipInvite) +func (store *SQLStateStore) GetRoomJoinedOrInvitedMembers(ctx context.Context, roomID id.RoomID) (members []id.UserID, err error) { + var memberMap map[id.UserID]*event.MemberEventContent + memberMap, err = store.GetRoomMembers(ctx, roomID, event.MembershipJoin, event.MembershipInvite) + if err != nil { + return + } members = make([]id.UserID, len(memberMap)) i := 0 for userID := range memberMap { @@ -103,37 +108,39 @@ func (store *SQLStateStore) GetRoomJoinedOrInvitedMembers(roomID id.RoomID) (mem return } -func (store *SQLStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership { - membership := event.MembershipLeave - err := store. - QueryRow("SELECT membership FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID). +func (store *SQLStateStore) GetMembership(ctx context.Context, roomID id.RoomID, userID id.UserID) (membership event.Membership, err error) { + err = store. + QueryRow(ctx, "SELECT membership FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID). Scan(&membership) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - store.Log.Warn("Failed to scan membership of %s in %s: %v", userID, roomID, err) + if errors.Is(err, sql.ErrNoRows) { + membership = event.MembershipLeave + err = nil } - return membership + return } -func (store *SQLStateStore) GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent { - member, ok := store.TryGetMember(roomID, userID) - if !ok { - member.Membership = event.MembershipLeave +func (store *SQLStateStore) GetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) { + member, err := store.TryGetMember(ctx, roomID, userID) + if member == nil && err == nil { + member = &event.MemberEventContent{Membership: event.MembershipLeave} } - return member + return member, err } -func (store *SQLStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, bool) { +func (store *SQLStateStore) TryGetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) { var member event.MemberEventContent err := store. - QueryRow("SELECT membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID). + QueryRow(ctx, "SELECT membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID). Scan(&member.Membership, &member.Displayname, &member.AvatarURL) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - store.Log.Warn("Failed to scan member info of %s in %s: %v", userID, roomID, err) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } else if err != nil { + return nil, err } - return &member, err == nil + return &member, nil } -func (store *SQLStateStore) FindSharedRooms(userID id.UserID) (rooms []id.RoomID) { +func (store *SQLStateStore) FindSharedRooms(ctx context.Context, userID id.UserID) ([]id.RoomID, error) { query := ` SELECT room_id FROM mx_user_profile LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id @@ -141,38 +148,32 @@ func (store *SQLStateStore) FindSharedRooms(userID id.UserID) (rooms []id.RoomID ` if !store.IsBridge { query = ` - SELECT mx_user_profile.room_id FROM mx_user_profile - LEFT JOIN mx_room_state ON mx_room_state.room_id=mx_user_profile.room_id - WHERE mx_user_profile.user_id=$1 AND mx_room_state.encryption IS NOT NULL - ` + SELECT mx_user_profile.room_id FROM mx_user_profile + LEFT JOIN mx_room_state ON mx_room_state.room_id=mx_user_profile.room_id + WHERE mx_user_profile.user_id=$1 AND mx_room_state.encryption IS NOT NULL + ` } - rows, err := store.Query(query, userID) + rows, err := store.Query(ctx, query, userID) if err != nil { - store.Log.Warn("Failed to query shared rooms with %s: %v", userID, err) - return + return nil, err } - for rows.Next() { - var roomID id.RoomID - err = rows.Scan(&roomID) - if err != nil { - store.Log.Warn("Failed to scan room ID: %v", err) - } else { - rooms = append(rooms, roomID) - } + return dbutil.NewRowIter(rows, dbutil.ScanSingleColumn[id.RoomID]).AsList() +} + +func (store *SQLStateStore) IsInRoom(ctx context.Context, roomID id.RoomID, userID id.UserID) bool { + return store.IsMembership(ctx, roomID, userID, "join") +} + +func (store *SQLStateStore) IsInvited(ctx context.Context, roomID id.RoomID, userID id.UserID) bool { + return store.IsMembership(ctx, roomID, userID, "join", "invite") +} + +func (store *SQLStateStore) IsMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool { + membership, err := store.GetMembership(ctx, roomID, userID) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get membership") + return false } - return -} - -func (store *SQLStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool { - return store.IsMembership(roomID, userID, "join") -} - -func (store *SQLStateStore) IsInvited(roomID id.RoomID, userID id.UserID) bool { - return store.IsMembership(roomID, userID, "join", "invite") -} - -func (store *SQLStateStore) IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool { - membership := store.GetMembership(roomID, userID) for _, allowedMembership := range allowedMemberships { if allowedMembership == membership { return true @@ -181,27 +182,23 @@ func (store *SQLStateStore) IsMembership(roomID id.RoomID, userID id.UserID, all return false } -func (store *SQLStateStore) SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) { - _, err := store.Exec(` +func (store *SQLStateStore) SetMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, membership event.Membership) error { + _, err := store.Exec(ctx, ` INSERT INTO mx_user_profile (room_id, user_id, membership, displayname, avatar_url) VALUES ($1, $2, $3, '', '') ON CONFLICT (room_id, user_id) DO UPDATE SET membership=excluded.membership `, roomID, userID, membership) - if err != nil { - store.Log.Warn("Failed to set membership of %s in %s to %s: %v", userID, roomID, membership, err) - } + return err } -func (store *SQLStateStore) SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) { - _, err := store.Exec(` +func (store *SQLStateStore) SetMember(ctx context.Context, roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) error { + _, err := store.Exec(ctx, ` INSERT INTO mx_user_profile (room_id, user_id, membership, displayname, avatar_url) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (room_id, user_id) DO UPDATE SET membership=excluded.membership, displayname=excluded.displayname, avatar_url=excluded.avatar_url `, roomID, userID, member.Membership, member.Displayname, member.AvatarURL) - if err != nil { - store.Log.Warn("Failed to set membership of %s in %s to %s: %v", userID, roomID, member, err) - } + return err } -func (store *SQLStateStore) ClearCachedMembers(roomID id.RoomID, memberships ...event.Membership) { +func (store *SQLStateStore) ClearCachedMembers(ctx context.Context, roomID id.RoomID, memberships ...event.Membership) error { query := "DELETE FROM mx_user_profile WHERE room_id=$1" params := make([]any, len(memberships)+1) params[0] = roomID @@ -213,109 +210,85 @@ func (store *SQLStateStore) ClearCachedMembers(roomID id.RoomID, memberships ... } query += fmt.Sprintf(" AND membership IN (%s)", strings.Join(placeholders, ",")) } - _, err := store.Exec(query, params...) - if err != nil { - store.Log.Warn("Failed to clear cached members of %s: %v", roomID, err) - } + _, err := store.Exec(ctx, query, params...) + return err } -func (store *SQLStateStore) SetEncryptionEvent(roomID id.RoomID, content *event.EncryptionEventContent) { +func (store *SQLStateStore) SetEncryptionEvent(ctx context.Context, roomID id.RoomID, content *event.EncryptionEventContent) error { contentBytes, err := json.Marshal(content) if err != nil { - store.Log.Warn("Failed to marshal encryption config of %s: %v", roomID, err) - return + return fmt.Errorf("failed to marshal content JSON: %w", err) } - _, err = store.Exec(` + _, err = store.Exec(ctx, ` INSERT INTO mx_room_state (room_id, encryption) VALUES ($1, $2) ON CONFLICT (room_id) DO UPDATE SET encryption=excluded.encryption `, roomID, contentBytes) - if err != nil { - store.Log.Warn("Failed to store encryption config of %s: %v", roomID, err) - } + return err } -func (store *SQLStateStore) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent { +func (store *SQLStateStore) GetEncryptionEvent(ctx context.Context, roomID id.RoomID) (*event.EncryptionEventContent, error) { var data []byte err := store. - QueryRow("SELECT encryption FROM mx_room_state WHERE room_id=$1", roomID). + QueryRow(ctx, "SELECT encryption FROM mx_room_state WHERE room_id=$1", roomID). Scan(&data) - if err != nil { - if !errors.Is(err, sql.ErrNoRows) { - store.Log.Warn("Failed to scan encryption config of %s: %v", roomID, err) - } - return nil + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } else if err != nil { + return nil, err } else if data == nil { - return nil + return nil, nil } - content := &event.EncryptionEventContent{} - err = json.Unmarshal(data, content) + var content event.EncryptionEventContent + err = json.Unmarshal(data, &content) if err != nil { - store.Log.Warn("Failed to parse encryption config of %s: %v", roomID, err) - return nil + return nil, fmt.Errorf("failed to parse content JSON: %w", err) } - return content + return &content, nil } -func (store *SQLStateStore) IsEncrypted(roomID id.RoomID) bool { - cfg := store.GetEncryptionEvent(roomID) - return cfg != nil && cfg.Algorithm == id.AlgorithmMegolmV1 +func (store *SQLStateStore) IsEncrypted(ctx context.Context, roomID id.RoomID) (bool, error) { + cfg, err := store.GetEncryptionEvent(ctx, roomID) + return cfg != nil && cfg.Algorithm == id.AlgorithmMegolmV1, err } -func (store *SQLStateStore) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) { - levelsBytes, err := json.Marshal(levels) - if err != nil { - store.Log.Warn("Failed to marshal power levels of %s: %v", roomID, err) - return - } - _, err = store.Exec(` +func (store *SQLStateStore) SetPowerLevels(ctx context.Context, roomID id.RoomID, levels *event.PowerLevelsEventContent) error { + _, err := store.Exec(ctx, ` INSERT INTO mx_room_state (room_id, power_levels) VALUES ($1, $2) ON CONFLICT (room_id) DO UPDATE SET power_levels=excluded.power_levels - `, roomID, levelsBytes) - if err != nil { - store.Log.Warn("Failed to store power levels of %s: %v", roomID, err) - } + `, roomID, dbutil.JSON{Data: levels}) + return err } -func (store *SQLStateStore) GetPowerLevels(roomID id.RoomID) (levels *event.PowerLevelsEventContent) { - var data []byte - err := store. - QueryRow("SELECT power_levels FROM mx_room_state WHERE room_id=$1", roomID). - Scan(&data) - if err != nil { - if !errors.Is(err, sql.ErrNoRows) { - store.Log.Warn("Failed to scan power levels of %s: %v", roomID, err) - } - return - } else if data == nil { - return - } - levels = &event.PowerLevelsEventContent{} - err = json.Unmarshal(data, levels) - if err != nil { - store.Log.Warn("Failed to parse power levels of %s: %v", roomID, err) - return nil +func (store *SQLStateStore) GetPowerLevels(ctx context.Context, roomID id.RoomID) (levels *event.PowerLevelsEventContent, err error) { + err = store. + QueryRow(ctx, "SELECT power_levels FROM mx_room_state WHERE room_id=$1", roomID). + Scan(&dbutil.JSON{Data: &levels}) + if errors.Is(err, sql.ErrNoRows) { + err = nil } return } -func (store *SQLStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int { +func (store *SQLStateStore) GetPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID) (int, error) { if store.Dialect == dbutil.Postgres { var powerLevel int err := store. - QueryRow(` + QueryRow(ctx, ` SELECT COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0) FROM mx_room_state WHERE room_id=$1 `, roomID, userID). Scan(&powerLevel) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - store.Log.Warn("Failed to scan power level of %s in %s: %v", userID, roomID, err) + return powerLevel, err + } else { + levels, err := store.GetPowerLevels(ctx, roomID) + if err != nil { + return 0, err } - return powerLevel + return levels.GetUserLevel(userID), nil } - return store.GetPowerLevels(roomID).GetUserLevel(userID) } -func (store *SQLStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int { +func (store *SQLStateStore) GetPowerLevelRequirement(ctx context.Context, roomID id.RoomID, eventType event.Type) (int, error) { if store.Dialect == dbutil.Postgres { defaultType := "events_default" defaultValue := 0 @@ -325,23 +298,26 @@ func (store *SQLStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType } var powerLevel int err := store. - QueryRow(` + QueryRow(ctx, ` SELECT COALESCE((power_levels->'events'->$2)::int, (power_levels->'$3')::int, $4) FROM mx_room_state WHERE room_id=$1 `, roomID, eventType.Type, defaultType, defaultValue). Scan(&powerLevel) - if err != nil { - if !errors.Is(err, sql.ErrNoRows) { - store.Log.Warn("Failed to scan power level for %s in %s: %v", eventType, roomID, err) - } - return defaultValue + if errors.Is(err, sql.ErrNoRows) { + err = nil + powerLevel = defaultValue } - return powerLevel + return powerLevel, err + } else { + levels, err := store.GetPowerLevels(ctx, roomID) + if err != nil { + return 0, err + } + return levels.GetEventLevel(eventType), nil } - return store.GetPowerLevels(roomID).GetEventLevel(eventType) } -func (store *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool { +func (store *SQLStateStore) HasPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID, eventType event.Type) (bool, error) { if store.Dialect == dbutil.Postgres { defaultType := "events_default" defaultValue := 0 @@ -351,19 +327,22 @@ func (store *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, ev } var hasPower bool err := store. - QueryRow(`SELECT + QueryRow(ctx, `SELECT COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0) >= COALESCE((power_levels->'events'->$3)::int, (power_levels->'$4')::int, $5) FROM mx_room_state WHERE room_id=$1`, roomID, userID, eventType.Type, defaultType, defaultValue). Scan(&hasPower) - if err != nil { - if !errors.Is(err, sql.ErrNoRows) { - store.Log.Warn("Failed to scan power level for %s in %s: %v", eventType, roomID, err) - } - return defaultValue == 0 + if errors.Is(err, sql.ErrNoRows) { + err = nil + hasPower = defaultValue == 0 } - return hasPower + return hasPower, err + } else { + levels, err := store.GetPowerLevels(ctx, roomID) + if err != nil { + return false, err + } + return levels.GetUserLevel(userID) >= levels.GetEventLevel(eventType), nil } - return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType) } diff --git a/vendor/maunium.net/go/mautrix/sqlstatestore/v05-mark-encryption-state-resync.go b/vendor/maunium.net/go/mautrix/sqlstatestore/v05-mark-encryption-state-resync.go index d66a9e9..bf44d30 100644 --- a/vendor/maunium.net/go/mautrix/sqlstatestore/v05-mark-encryption-state-resync.go +++ b/vendor/maunium.net/go/mautrix/sqlstatestore/v05-mark-encryption-state-resync.go @@ -1,19 +1,20 @@ package sqlstatestore import ( + "context" "fmt" "go.mau.fi/util/dbutil" ) func init() { - UpgradeTable.Register(-1, 5, 0, "Mark rooms that need crypto state event resynced", true, func(tx dbutil.Execable, db *dbutil.Database) error { - portalExists, err := db.TableExists(tx, "portal") + UpgradeTable.Register(-1, 5, 0, "Mark rooms that need crypto state event resynced", true, func(ctx context.Context, db *dbutil.Database) error { + portalExists, err := db.TableExists(ctx, "portal") if err != nil { return fmt.Errorf("failed to check if portal table exists") } if portalExists { - _, err = tx.Exec(` + _, err = db.Exec(ctx, ` INSERT INTO mx_room_state (room_id, encryption) SELECT portal.mxid, '{"resync":true}' FROM portal WHERE portal.encrypted=true AND portal.mxid IS NOT NULL ON CONFLICT (room_id) DO UPDATE diff --git a/vendor/maunium.net/go/mautrix/statestore.go b/vendor/maunium.net/go/mautrix/statestore.go index 2c0a8fd..8fe5f8b 100644 --- a/vendor/maunium.net/go/mautrix/statestore.go +++ b/vendor/maunium.net/go/mautrix/statestore.go @@ -7,33 +7,37 @@ package mautrix import ( + "context" "sync" + "github.com/rs/zerolog" + "go.mau.fi/util/exerrors" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) // StateStore is an interface for storing basic room state information. type StateStore interface { - IsInRoom(roomID id.RoomID, userID id.UserID) bool - IsInvited(roomID id.RoomID, userID id.UserID) bool - IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool - GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent - TryGetMember(roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, bool) - SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) - SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) - ClearCachedMembers(roomID id.RoomID, memberships ...event.Membership) + IsInRoom(ctx context.Context, roomID id.RoomID, userID id.UserID) bool + IsInvited(ctx context.Context, roomID id.RoomID, userID id.UserID) bool + IsMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool + GetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) + TryGetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) + SetMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, membership event.Membership) error + SetMember(ctx context.Context, roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) error + ClearCachedMembers(ctx context.Context, roomID id.RoomID, memberships ...event.Membership) error - SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) - GetPowerLevels(roomID id.RoomID) *event.PowerLevelsEventContent + SetPowerLevels(ctx context.Context, roomID id.RoomID, levels *event.PowerLevelsEventContent) error + GetPowerLevels(ctx context.Context, roomID id.RoomID) (*event.PowerLevelsEventContent, error) - SetEncryptionEvent(roomID id.RoomID, content *event.EncryptionEventContent) - IsEncrypted(roomID id.RoomID) bool + SetEncryptionEvent(ctx context.Context, roomID id.RoomID, content *event.EncryptionEventContent) error + IsEncrypted(ctx context.Context, roomID id.RoomID) (bool, error) - GetRoomJoinedOrInvitedMembers(roomID id.RoomID) ([]id.UserID, error) + GetRoomJoinedOrInvitedMembers(ctx context.Context, roomID id.RoomID) ([]id.UserID, error) } -func UpdateStateStore(store StateStore, evt *event.Event) { +func UpdateStateStore(ctx context.Context, store StateStore, evt *event.Event) { if store == nil || evt == nil || evt.StateKey == nil { return } @@ -41,13 +45,20 @@ func UpdateStateStore(store StateStore, evt *event.Event) { if evt.Type != event.StateMember && evt.GetStateKey() != "" { return } + var err error switch content := evt.Content.Parsed.(type) { case *event.MemberEventContent: - store.SetMember(evt.RoomID, id.UserID(evt.GetStateKey()), content) + err = store.SetMember(ctx, evt.RoomID, id.UserID(evt.GetStateKey()), content) case *event.PowerLevelsEventContent: - store.SetPowerLevels(evt.RoomID, content) + err = store.SetPowerLevels(ctx, evt.RoomID, content) case *event.EncryptionEventContent: - store.SetEncryptionEvent(evt.RoomID, content) + err = store.SetEncryptionEvent(ctx, evt.RoomID, content) + } + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err). + Stringer("event_id", evt.ID). + Str("event_type", evt.Type.Type). + Msg("Failed to update state store") } } @@ -56,8 +67,8 @@ func UpdateStateStore(store StateStore, evt *event.Event) { // client.Syncer.(mautrix.ExtensibleSyncer).OnEvent(client.StateStoreSyncHandler) // // DefaultSyncer.ParseEventContent must also be true for this to work (which it is by default). -func (cli *Client) StateStoreSyncHandler(_ EventSource, evt *event.Event) { - UpdateStateStore(cli.StateStore, evt) +func (cli *Client) StateStoreSyncHandler(ctx context.Context, evt *event.Event) { + UpdateStateStore(ctx, cli.StateStore, evt) } type MemoryStateStore struct { @@ -81,20 +92,21 @@ func NewMemoryStateStore() StateStore { } } -func (store *MemoryStateStore) IsRegistered(userID id.UserID) bool { +func (store *MemoryStateStore) IsRegistered(_ context.Context, userID id.UserID) (bool, error) { store.registrationsLock.RLock() defer store.registrationsLock.RUnlock() registered, ok := store.Registrations[userID] - return ok && registered + return ok && registered, nil } -func (store *MemoryStateStore) MarkRegistered(userID id.UserID) { +func (store *MemoryStateStore) MarkRegistered(_ context.Context, userID id.UserID) error { store.registrationsLock.Lock() defer store.registrationsLock.Unlock() store.Registrations[userID] = true + return nil } -func (store *MemoryStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*event.MemberEventContent { +func (store *MemoryStateStore) GetRoomMembers(_ context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) { store.membersLock.RLock() members, ok := store.Members[roomID] store.membersLock.RUnlock() @@ -104,11 +116,14 @@ func (store *MemoryStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*e store.Members[roomID] = members store.membersLock.Unlock() } - return members + return members, nil } -func (store *MemoryStateStore) GetRoomJoinedOrInvitedMembers(roomID id.RoomID) ([]id.UserID, error) { - members := store.GetRoomMembers(roomID) +func (store *MemoryStateStore) GetRoomJoinedOrInvitedMembers(ctx context.Context, roomID id.RoomID) ([]id.UserID, error) { + members, err := store.GetRoomMembers(ctx, roomID) + if err != nil { + return nil, err + } ids := make([]id.UserID, 0, len(members)) for id := range members { ids = append(ids, id) @@ -116,39 +131,39 @@ func (store *MemoryStateStore) GetRoomJoinedOrInvitedMembers(roomID id.RoomID) ( return ids, nil } -func (store *MemoryStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership { - return store.GetMember(roomID, userID).Membership +func (store *MemoryStateStore) GetMembership(ctx context.Context, roomID id.RoomID, userID id.UserID) (event.Membership, error) { + return exerrors.Must(store.GetMember(ctx, roomID, userID)).Membership, nil } -func (store *MemoryStateStore) GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent { - member, ok := store.TryGetMember(roomID, userID) - if !ok { +func (store *MemoryStateStore) GetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) { + member, err := store.TryGetMember(ctx, roomID, userID) + if member == nil && err == nil { member = &event.MemberEventContent{Membership: event.MembershipLeave} } - return member + return member, err } -func (store *MemoryStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (member *event.MemberEventContent, ok bool) { +func (store *MemoryStateStore) TryGetMember(_ context.Context, roomID id.RoomID, userID id.UserID) (member *event.MemberEventContent, err error) { store.membersLock.RLock() defer store.membersLock.RUnlock() members, membersOk := store.Members[roomID] if !membersOk { return } - member, ok = members[userID] + member = members[userID] return } -func (store *MemoryStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool { - return store.IsMembership(roomID, userID, "join") +func (store *MemoryStateStore) IsInRoom(ctx context.Context, roomID id.RoomID, userID id.UserID) bool { + return store.IsMembership(ctx, roomID, userID, "join") } -func (store *MemoryStateStore) IsInvited(roomID id.RoomID, userID id.UserID) bool { - return store.IsMembership(roomID, userID, "join", "invite") +func (store *MemoryStateStore) IsInvited(ctx context.Context, roomID id.RoomID, userID id.UserID) bool { + return store.IsMembership(ctx, roomID, userID, "join", "invite") } -func (store *MemoryStateStore) IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool { - membership := store.GetMembership(roomID, userID) +func (store *MemoryStateStore) IsMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool { + membership := exerrors.Must(store.GetMembership(ctx, roomID, userID)) for _, allowedMembership := range allowedMemberships { if allowedMembership == membership { return true @@ -157,7 +172,7 @@ func (store *MemoryStateStore) IsMembership(roomID id.RoomID, userID id.UserID, return false } -func (store *MemoryStateStore) SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) { +func (store *MemoryStateStore) SetMembership(_ context.Context, roomID id.RoomID, userID id.UserID, membership event.Membership) error { store.membersLock.Lock() members, ok := store.Members[roomID] if !ok { @@ -175,9 +190,10 @@ func (store *MemoryStateStore) SetMembership(roomID id.RoomID, userID id.UserID, } store.Members[roomID] = members store.membersLock.Unlock() + return nil } -func (store *MemoryStateStore) SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) { +func (store *MemoryStateStore) SetMember(_ context.Context, roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) error { store.membersLock.Lock() members, ok := store.Members[roomID] if !ok { @@ -189,14 +205,15 @@ func (store *MemoryStateStore) SetMember(roomID id.RoomID, userID id.UserID, mem } store.Members[roomID] = members store.membersLock.Unlock() + return nil } -func (store *MemoryStateStore) ClearCachedMembers(roomID id.RoomID, memberships ...event.Membership) { +func (store *MemoryStateStore) ClearCachedMembers(_ context.Context, roomID id.RoomID, memberships ...event.Membership) error { store.membersLock.Lock() defer store.membersLock.Unlock() members, ok := store.Members[roomID] if !ok { - return + return nil } for userID, member := range members { for _, membership := range memberships { @@ -206,46 +223,49 @@ func (store *MemoryStateStore) ClearCachedMembers(roomID id.RoomID, memberships } } } + return nil } -func (store *MemoryStateStore) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) { +func (store *MemoryStateStore) SetPowerLevels(_ context.Context, roomID id.RoomID, levels *event.PowerLevelsEventContent) error { store.powerLevelsLock.Lock() store.PowerLevels[roomID] = levels store.powerLevelsLock.Unlock() + return nil } -func (store *MemoryStateStore) GetPowerLevels(roomID id.RoomID) (levels *event.PowerLevelsEventContent) { +func (store *MemoryStateStore) GetPowerLevels(_ context.Context, roomID id.RoomID) (levels *event.PowerLevelsEventContent, err error) { store.powerLevelsLock.RLock() levels = store.PowerLevels[roomID] store.powerLevelsLock.RUnlock() return } -func (store *MemoryStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int { - return store.GetPowerLevels(roomID).GetUserLevel(userID) +func (store *MemoryStateStore) GetPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID) (int, error) { + return exerrors.Must(store.GetPowerLevels(ctx, roomID)).GetUserLevel(userID), nil } -func (store *MemoryStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int { - return store.GetPowerLevels(roomID).GetEventLevel(eventType) +func (store *MemoryStateStore) GetPowerLevelRequirement(ctx context.Context, roomID id.RoomID, eventType event.Type) (int, error) { + return exerrors.Must(store.GetPowerLevels(ctx, roomID)).GetEventLevel(eventType), nil } -func (store *MemoryStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool { - return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType) +func (store *MemoryStateStore) HasPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID, eventType event.Type) (bool, error) { + return exerrors.Must(store.GetPowerLevel(ctx, roomID, userID)) >= exerrors.Must(store.GetPowerLevelRequirement(ctx, roomID, eventType)), nil } -func (store *MemoryStateStore) SetEncryptionEvent(roomID id.RoomID, content *event.EncryptionEventContent) { +func (store *MemoryStateStore) SetEncryptionEvent(_ context.Context, roomID id.RoomID, content *event.EncryptionEventContent) error { store.encryptionLock.Lock() store.Encryption[roomID] = content store.encryptionLock.Unlock() + return nil } -func (store *MemoryStateStore) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent { +func (store *MemoryStateStore) GetEncryptionEvent(_ context.Context, roomID id.RoomID) (*event.EncryptionEventContent, error) { store.encryptionLock.RLock() defer store.encryptionLock.RUnlock() - return store.Encryption[roomID] + return store.Encryption[roomID], nil } -func (store *MemoryStateStore) IsEncrypted(roomID id.RoomID) bool { - cfg := store.GetEncryptionEvent(roomID) - return cfg != nil && cfg.Algorithm == id.AlgorithmMegolmV1 +func (store *MemoryStateStore) IsEncrypted(ctx context.Context, roomID id.RoomID) (bool, error) { + cfg, err := store.GetEncryptionEvent(ctx, roomID) + return cfg != nil && cfg.Algorithm == id.AlgorithmMegolmV1, err } diff --git a/vendor/maunium.net/go/mautrix/sync.go b/vendor/maunium.net/go/mautrix/sync.go index 5979a17..d420840 100644 --- a/vendor/maunium.net/go/mautrix/sync.go +++ b/vendor/maunium.net/go/mautrix/sync.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Tulir Asokan +// Copyright (c) 2024 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,6 +7,7 @@ package mautrix import ( + "context" "errors" "fmt" "runtime/debug" @@ -16,78 +17,17 @@ import ( "maunium.net/go/mautrix/id" ) -// EventSource represents the part of the sync response that an event came from. -type EventSource int - -const ( - EventSourcePresence EventSource = 1 << iota - EventSourceJoin - EventSourceInvite - EventSourceLeave - EventSourceAccountData - EventSourceTimeline - EventSourceState - EventSourceEphemeral - EventSourceToDevice - EventSourceDecrypted -) - -const primaryTypes = EventSourcePresence | EventSourceAccountData | EventSourceToDevice | EventSourceTimeline | EventSourceState -const roomSections = EventSourceJoin | EventSourceInvite | EventSourceLeave -const roomableTypes = EventSourceAccountData | EventSourceTimeline | EventSourceState -const encryptableTypes = roomableTypes | EventSourceToDevice - -func (es EventSource) String() string { - var typeName string - switch es & primaryTypes { - case EventSourcePresence: - typeName = "presence" - case EventSourceAccountData: - typeName = "account data" - case EventSourceToDevice: - typeName = "to-device" - case EventSourceTimeline: - typeName = "timeline" - case EventSourceState: - typeName = "state" - default: - return fmt.Sprintf("unknown (%d)", es) - } - if es&roomableTypes != 0 { - switch es & roomSections { - case EventSourceJoin: - typeName = "joined room " + typeName - case EventSourceInvite: - typeName = "invited room " + typeName - case EventSourceLeave: - typeName = "left room " + typeName - default: - return fmt.Sprintf("unknown (%s+%d)", typeName, es) - } - es &^= roomSections - } - if es&encryptableTypes != 0 && es&EventSourceDecrypted != 0 { - typeName += " (decrypted)" - es &^= EventSourceDecrypted - } - es &^= primaryTypes - if es != 0 { - return fmt.Sprintf("unknown (%s+%d)", typeName, es) - } - return typeName -} - // EventHandler handles a single event from a sync response. -type EventHandler func(source EventSource, evt *event.Event) +type EventHandler func(ctx context.Context, evt *event.Event) // SyncHandler handles a whole sync response. If the return value is false, handling will be stopped completely. -type SyncHandler func(resp *RespSync, since string) bool +type SyncHandler func(ctx context.Context, resp *RespSync, since string) bool // Syncer is an interface that must be satisfied in order to do /sync requests on a client. type Syncer interface { // ProcessResponse processes the /sync response. The since parameter is the since= value that was used to produce the response. // This is useful for detecting the very first sync (since=""). If an error is return, Syncing will be stopped permanently. - ProcessResponse(resp *RespSync, since string) error + ProcessResponse(ctx context.Context, resp *RespSync, since string) error // OnFailedSync returns either the time to wait before retrying or an error to stop syncing permanently. OnFailedSync(res *RespSync, err error) (time.Duration, error) // GetFilterJSON for the given user ID. NOT the filter ID. @@ -101,7 +41,7 @@ type ExtensibleSyncer interface { } type DispatchableSyncer interface { - Dispatch(source EventSource, evt *event.Event) + Dispatch(ctx context.Context, evt *event.Event) } // DefaultSyncer is the default syncing implementation. You can either write your own syncer, or selectively @@ -134,14 +74,17 @@ func NewDefaultSyncer() *DefaultSyncer { globalListeners: []EventHandler{}, ParseEventContent: true, ParseErrorHandler: func(evt *event.Event, err error) bool { - return false + // By default, drop known events that can't be parsed, but let unknown events through + return errors.Is(err, event.ErrUnsupportedContentType) || + // Also allow events that had their content already parsed by some other function + errors.Is(err, event.ErrContentAlreadyParsed) }, } } // ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of // unrepeating events. Returns a fatal error if a listener panics. -func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error) { +func (s *DefaultSyncer) ProcessResponse(ctx context.Context, res *RespSync, since string) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("ProcessResponse panicked! since=%s panic=%s\n%s", since, r, debug.Stack()) @@ -149,38 +92,38 @@ func (s *DefaultSyncer) ProcessResponse(res *RespSync, since string) (err error) }() for _, listener := range s.syncListeners { - if !listener(res, since) { + if !listener(ctx, res, since) { return } } - s.processSyncEvents("", res.ToDevice.Events, EventSourceToDevice) - s.processSyncEvents("", res.Presence.Events, EventSourcePresence) - s.processSyncEvents("", res.AccountData.Events, EventSourceAccountData) + s.processSyncEvents(ctx, "", res.ToDevice.Events, event.SourceToDevice) + s.processSyncEvents(ctx, "", res.Presence.Events, event.SourcePresence) + s.processSyncEvents(ctx, "", res.AccountData.Events, event.SourceAccountData) for roomID, roomData := range res.Rooms.Join { - s.processSyncEvents(roomID, roomData.State.Events, EventSourceJoin|EventSourceState) - s.processSyncEvents(roomID, roomData.Timeline.Events, EventSourceJoin|EventSourceTimeline) - s.processSyncEvents(roomID, roomData.Ephemeral.Events, EventSourceJoin|EventSourceEphemeral) - s.processSyncEvents(roomID, roomData.AccountData.Events, EventSourceJoin|EventSourceAccountData) + s.processSyncEvents(ctx, roomID, roomData.State.Events, event.SourceJoin|event.SourceState) + s.processSyncEvents(ctx, roomID, roomData.Timeline.Events, event.SourceJoin|event.SourceTimeline) + s.processSyncEvents(ctx, roomID, roomData.Ephemeral.Events, event.SourceJoin|event.SourceEphemeral) + s.processSyncEvents(ctx, roomID, roomData.AccountData.Events, event.SourceJoin|event.SourceAccountData) } for roomID, roomData := range res.Rooms.Invite { - s.processSyncEvents(roomID, roomData.State.Events, EventSourceInvite|EventSourceState) + s.processSyncEvents(ctx, roomID, roomData.State.Events, event.SourceInvite|event.SourceState) } for roomID, roomData := range res.Rooms.Leave { - s.processSyncEvents(roomID, roomData.State.Events, EventSourceLeave|EventSourceState) - s.processSyncEvents(roomID, roomData.Timeline.Events, EventSourceLeave|EventSourceTimeline) + s.processSyncEvents(ctx, roomID, roomData.State.Events, event.SourceLeave|event.SourceState) + s.processSyncEvents(ctx, roomID, roomData.Timeline.Events, event.SourceLeave|event.SourceTimeline) } return } -func (s *DefaultSyncer) processSyncEvents(roomID id.RoomID, events []*event.Event, source EventSource) { +func (s *DefaultSyncer) processSyncEvents(ctx context.Context, roomID id.RoomID, events []*event.Event, source event.Source) { for _, evt := range events { - s.processSyncEvent(roomID, evt, source) + s.processSyncEvent(ctx, roomID, evt, source) } } -func (s *DefaultSyncer) processSyncEvent(roomID id.RoomID, evt *event.Event, source EventSource) { +func (s *DefaultSyncer) processSyncEvent(ctx context.Context, roomID id.RoomID, evt *event.Event, source event.Source) { evt.RoomID = roomID // Ensure the type class is correct. It's safe to mutate the class since the event type is not a pointer. @@ -188,11 +131,11 @@ func (s *DefaultSyncer) processSyncEvent(roomID id.RoomID, evt *event.Event, sou switch { case evt.StateKey != nil: evt.Type.Class = event.StateEventType - case source == EventSourcePresence, source&EventSourceEphemeral != 0: + case source == event.SourcePresence, source&event.SourceEphemeral != 0: evt.Type.Class = event.EphemeralEventType - case source&EventSourceAccountData != 0: + case source&event.SourceAccountData != 0: evt.Type.Class = event.AccountDataEventType - case source == EventSourceToDevice: + case source == event.SourceToDevice: evt.Type.Class = event.ToDeviceEventType default: evt.Type.Class = event.MessageEventType @@ -205,17 +148,18 @@ func (s *DefaultSyncer) processSyncEvent(roomID id.RoomID, evt *event.Event, sou } } - s.Dispatch(source, evt) + evt.Mautrix.EventSource = source + s.Dispatch(ctx, evt) } -func (s *DefaultSyncer) Dispatch(source EventSource, evt *event.Event) { +func (s *DefaultSyncer) Dispatch(ctx context.Context, evt *event.Event) { for _, fn := range s.globalListeners { - fn(source, evt) + fn(ctx, evt) } listeners, exists := s.listeners[evt.Type] if exists { for _, fn := range listeners { - fn(source, evt) + fn(ctx, evt) } } } @@ -263,31 +207,18 @@ func (s *DefaultSyncer) GetFilterJSON(userID id.UserID) *Filter { return s.FilterJSON } -// OldEventIgnorer is a utility struct for bots to ignore events from before the bot joined the room. -// -// Deprecated: Use Client.DontProcessOldEvents instead. -type OldEventIgnorer struct { - UserID id.UserID -} - -func (oei *OldEventIgnorer) Register(syncer ExtensibleSyncer) { - syncer.OnSync(oei.DontProcessOldEvents) -} - -func (oei *OldEventIgnorer) DontProcessOldEvents(resp *RespSync, since string) bool { - return dontProcessOldEvents(oei.UserID, resp, since) -} - // DontProcessOldEvents is a sync handler that removes rooms that the user just joined. // It's meant for bots to ignore events from before the bot joined the room. // // To use it, register it with your Syncer, e.g.: // // cli.Syncer.(mautrix.ExtensibleSyncer).OnSync(cli.DontProcessOldEvents) -func (cli *Client) DontProcessOldEvents(resp *RespSync, since string) bool { +func (cli *Client) DontProcessOldEvents(_ context.Context, resp *RespSync, since string) bool { return dontProcessOldEvents(cli.UserID, resp, since) } +var _ SyncHandler = (*Client)(nil).DontProcessOldEvents + func dontProcessOldEvents(userID id.UserID, resp *RespSync, since string) bool { if since == "" { return false @@ -324,7 +255,7 @@ func dontProcessOldEvents(userID id.UserID, resp *RespSync, since string) bool { // To use it, register it with your Syncer, e.g.: // // cli.Syncer.(mautrix.ExtensibleSyncer).OnSync(cli.MoveInviteState) -func (cli *Client) MoveInviteState(resp *RespSync, _ string) bool { +func (cli *Client) MoveInviteState(ctx context.Context, resp *RespSync, _ string) bool { for _, meta := range resp.Rooms.Invite { var inviteState []event.StrippedState var inviteEvt *event.Event @@ -349,3 +280,5 @@ func (cli *Client) MoveInviteState(resp *RespSync, _ string) bool { } return true } + +var _ SyncHandler = (*Client)(nil).MoveInviteState diff --git a/vendor/maunium.net/go/mautrix/syncstore.go b/vendor/maunium.net/go/mautrix/syncstore.go index d5fe2db..6c7fc9e 100644 --- a/vendor/maunium.net/go/mautrix/syncstore.go +++ b/vendor/maunium.net/go/mautrix/syncstore.go @@ -1,21 +1,26 @@ package mautrix import ( + "context" "errors" + "fmt" "maunium.net/go/mautrix/id" ) +var _ SyncStore = (*MemorySyncStore)(nil) +var _ SyncStore = (*AccountDataStore)(nil) + // SyncStore is an interface which must be satisfied to store client data. // // You can either write a struct which persists this data to disk, or you can use the // provided "MemorySyncStore" which just keeps data around in-memory which is lost on // restarts. type SyncStore interface { - SaveFilterID(userID id.UserID, filterID string) - LoadFilterID(userID id.UserID) string - SaveNextBatch(userID id.UserID, nextBatchToken string) - LoadNextBatch(userID id.UserID) string + SaveFilterID(ctx context.Context, userID id.UserID, filterID string) error + LoadFilterID(ctx context.Context, userID id.UserID) (string, error) + SaveNextBatch(ctx context.Context, userID id.UserID, nextBatchToken string) error + LoadNextBatch(ctx context.Context, userID id.UserID) (string, error) } // Deprecated: renamed to SyncStore @@ -32,23 +37,25 @@ type MemorySyncStore struct { } // SaveFilterID to memory. -func (s *MemorySyncStore) SaveFilterID(userID id.UserID, filterID string) { +func (s *MemorySyncStore) SaveFilterID(ctx context.Context, userID id.UserID, filterID string) error { s.Filters[userID] = filterID + return nil } // LoadFilterID from memory. -func (s *MemorySyncStore) LoadFilterID(userID id.UserID) string { - return s.Filters[userID] +func (s *MemorySyncStore) LoadFilterID(ctx context.Context, userID id.UserID) (string, error) { + return s.Filters[userID], nil } // SaveNextBatch to memory. -func (s *MemorySyncStore) SaveNextBatch(userID id.UserID, nextBatchToken string) { +func (s *MemorySyncStore) SaveNextBatch(ctx context.Context, userID id.UserID, nextBatchToken string) error { s.NextBatch[userID] = nextBatchToken + return nil } // LoadNextBatch from memory. -func (s *MemorySyncStore) LoadNextBatch(userID id.UserID) string { - return s.NextBatch[userID] +func (s *MemorySyncStore) LoadNextBatch(ctx context.Context, userID id.UserID) (string, error) { + return s.NextBatch[userID], nil } // NewMemorySyncStore constructs a new MemorySyncStore. @@ -72,34 +79,35 @@ type accountData struct { NextBatch string `json:"next_batch"` } -func (s *AccountDataStore) SaveFilterID(userID id.UserID, filterID string) { +func (s *AccountDataStore) SaveFilterID(ctx context.Context, userID id.UserID, filterID string) error { if userID.String() != s.client.UserID.String() { panic("AccountDataStore must only be used with a single account") } s.FilterID = filterID + return nil } -func (s *AccountDataStore) LoadFilterID(userID id.UserID) string { +func (s *AccountDataStore) LoadFilterID(ctx context.Context, userID id.UserID) (string, error) { if userID.String() != s.client.UserID.String() { panic("AccountDataStore must only be used with a single account") } - return s.FilterID + return s.FilterID, nil } -func (s *AccountDataStore) SaveNextBatch(userID id.UserID, nextBatchToken string) { +func (s *AccountDataStore) SaveNextBatch(ctx context.Context, userID id.UserID, nextBatchToken string) error { if userID.String() != s.client.UserID.String() { panic("AccountDataStore must only be used with a single account") } else if nextBatchToken == s.nextBatch { - return + return nil } data := accountData{ NextBatch: nextBatchToken, } - err := s.client.SetAccountData(s.EventType, data) + err := s.client.SetAccountData(ctx, s.EventType, data) if err != nil { - s.client.Log.Warn().Err(err).Msg("Failed to save next batch token to account data") + return fmt.Errorf("failed to save next batch token to account data: %w", err) } else { s.client.Log.Debug(). Str("old_token", s.nextBatch). @@ -107,28 +115,29 @@ func (s *AccountDataStore) SaveNextBatch(userID id.UserID, nextBatchToken string Msg("Saved next batch token") s.nextBatch = nextBatchToken } + return nil } -func (s *AccountDataStore) LoadNextBatch(userID id.UserID) string { +func (s *AccountDataStore) LoadNextBatch(ctx context.Context, userID id.UserID) (string, error) { if userID.String() != s.client.UserID.String() { panic("AccountDataStore must only be used with a single account") } data := &accountData{} - err := s.client.GetAccountData(s.EventType, data) + err := s.client.GetAccountData(ctx, s.EventType, data) if err != nil { if errors.Is(err, MNotFound) { s.client.Log.Debug().Msg("No next batch token found in account data") + return "", nil } else { - s.client.Log.Warn().Err(err).Msg("Failed to load next batch token from account data") + return "", fmt.Errorf("failed to load next batch token from account data: %w", err) } - return "" } s.nextBatch = data.NextBatch s.client.Log.Debug().Str("next_batch", data.NextBatch).Msg("Loaded next batch token from account data") - return s.nextBatch + return s.nextBatch, nil } // NewAccountDataStore returns a new AccountDataStore, which stores diff --git a/vendor/maunium.net/go/mautrix/version.go b/vendor/maunium.net/go/mautrix/version.go index 7b0c3db..d92a797 100644 --- a/vendor/maunium.net/go/mautrix/version.go +++ b/vendor/maunium.net/go/mautrix/version.go @@ -7,7 +7,7 @@ import ( "strings" ) -const Version = "v0.16.2" +const Version = "v0.17.0" var GoModVersion = "" var Commit = "" diff --git a/vendor/maunium.net/go/mautrix/versions.go b/vendor/maunium.net/go/mautrix/versions.go index 2f898b7..d3dd3c6 100644 --- a/vendor/maunium.net/go/mautrix/versions.go +++ b/vendor/maunium.net/go/mautrix/versions.go @@ -93,6 +93,8 @@ var ( SpecV15 = MustParseSpecVersion("v1.5") SpecV16 = MustParseSpecVersion("v1.6") SpecV17 = MustParseSpecVersion("v1.7") + SpecV18 = MustParseSpecVersion("v1.8") + SpecV19 = MustParseSpecVersion("v1.9") ) func (svf SpecVersionFormat) String() string { diff --git a/vendor/modules.txt b/vendor/modules.txt index 5b6bfd6..f74c49a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -93,7 +93,7 @@ github.com/raja/argon2pw # github.com/rivo/uniseg v0.2.0 ## explicit; go 1.12 github.com/rivo/uniseg -# github.com/rs/zerolog v1.31.0 +# github.com/rs/zerolog v1.32.0 ## explicit; go 1.15 github.com/rs/zerolog github.com/rs/zerolog/internal/cbor @@ -113,8 +113,8 @@ github.com/tidwall/pretty # github.com/tidwall/sjson v1.2.5 ## explicit; go 1.14 github.com/tidwall/sjson -# github.com/yuin/goldmark v1.6.0 -## explicit; go 1.18 +# github.com/yuin/goldmark v1.7.0 +## explicit; go 1.19 github.com/yuin/goldmark github.com/yuin/goldmark/ast github.com/yuin/goldmark/extension @@ -124,7 +124,7 @@ github.com/yuin/goldmark/renderer github.com/yuin/goldmark/renderer/html github.com/yuin/goldmark/text github.com/yuin/goldmark/util -# gitlab.com/etke.cc/go/env v1.0.0 +# gitlab.com/etke.cc/go/env v1.1.0 ## explicit; go 1.18 gitlab.com/etke.cc/go/env # gitlab.com/etke.cc/go/fswatcher v1.0.0 @@ -145,10 +145,10 @@ gitlab.com/etke.cc/go/trysmtp # gitlab.com/etke.cc/go/validator v1.0.6 ## explicit; go 1.18 gitlab.com/etke.cc/go/validator -# gitlab.com/etke.cc/linkpearl v0.0.0-20231121221431-72443f33d266 +# gitlab.com/etke.cc/linkpearl v0.0.0-20240211143445-bddf907d137a ## explicit; go 1.18 gitlab.com/etke.cc/linkpearl -# go.mau.fi/util v0.2.1 +# go.mau.fi/util v0.3.0 ## explicit; go 1.20 go.mau.fi/util/base58 go.mau.fi/util/dbutil @@ -157,7 +157,7 @@ go.mau.fi/util/exgjson go.mau.fi/util/jsontime go.mau.fi/util/random go.mau.fi/util/retryafter -# golang.org/x/crypto v0.15.0 +# golang.org/x/crypto v0.19.0 ## explicit; go 1.18 golang.org/x/crypto/argon2 golang.org/x/crypto/blake2b @@ -172,18 +172,18 @@ golang.org/x/crypto/internal/poly1305 golang.org/x/crypto/pbkdf2 golang.org/x/crypto/ssh golang.org/x/crypto/ssh/internal/bcrypt_pbkdf -# golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa +# golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 ## explicit; go 1.20 golang.org/x/exp/constraints golang.org/x/exp/maps golang.org/x/exp/slices -# golang.org/x/net v0.18.0 +# golang.org/x/net v0.21.0 ## explicit; go 1.18 golang.org/x/net/context golang.org/x/net/html golang.org/x/net/html/atom golang.org/x/net/publicsuffix -# golang.org/x/sys v0.14.0 +# golang.org/x/sys v0.17.0 ## explicit; go 1.18 golang.org/x/sys/cpu golang.org/x/sys/execabs @@ -207,13 +207,25 @@ golang.org/x/text/transform ## explicit; go 1.19 maunium.net/go/maulogger/v2 maunium.net/go/maulogger/v2/maulogadapt -# maunium.net/go/mautrix v0.16.2 +# maunium.net/go/mautrix v0.17.0 ## explicit; go 1.20 maunium.net/go/mautrix maunium.net/go/mautrix/crypto maunium.net/go/mautrix/crypto/attachment maunium.net/go/mautrix/crypto/canonicaljson maunium.net/go/mautrix/crypto/cryptohelper +maunium.net/go/mautrix/crypto/goolm +maunium.net/go/mautrix/crypto/goolm/account +maunium.net/go/mautrix/crypto/goolm/cipher +maunium.net/go/mautrix/crypto/goolm/crypto +maunium.net/go/mautrix/crypto/goolm/libolmpickle +maunium.net/go/mautrix/crypto/goolm/megolm +maunium.net/go/mautrix/crypto/goolm/message +maunium.net/go/mautrix/crypto/goolm/olm +maunium.net/go/mautrix/crypto/goolm/pk +maunium.net/go/mautrix/crypto/goolm/sas +maunium.net/go/mautrix/crypto/goolm/session +maunium.net/go/mautrix/crypto/goolm/utilities maunium.net/go/mautrix/crypto/olm maunium.net/go/mautrix/crypto/sql_store_upgrade maunium.net/go/mautrix/crypto/ssss