updated deps; updated healthchecks.io integration

This commit is contained in:
Aine
2024-04-07 14:42:12 +03:00
parent 271a4a0e31
commit 15d61f174e
122 changed files with 3432 additions and 4613 deletions

View File

@@ -17,3 +17,8 @@ repos:
- "maunium.net/go/mautrix"
- "-w"
- id: go-vet-repo-mod
- repo: https://github.com/beeper/pre-commit-go
rev: v0.3.1
hooks:
- id: prevent-literal-http-methods

View File

@@ -1,9 +1,59 @@
## v0.18.0 (2024-03-16)
* **Breaking change *(client, bridge, appservice)*** Dropped support for
maulogger. Only zerolog loggers are now provided by default.
* *(bridge)* Fixed upload size limit not having a default if the server
returned no value.
* *(synapseadmin)* Added wrappers for some room and user admin APIs.
(thanks to [@grvn-ht] in [#181]).
* *(crypto/verificationhelper)* Fixed bugs.
* *(crypto)* Fixed key backup uploading doing too much base64.
* *(crypto)* Changed `EncryptMegolmEvent` to return an error if persisting the
megolm session fails. This ensures that database errors won't cause messages
to be sent with duplicate indexes.
* *(crypto)* Changed `GetOrRequestSecret` to use a callback instead of returning
the value directly. This allows validating the value in order to ignore
invalid secrets.
* *(id)* Added `ParseCommonIdentifier` function to parse any Matrix identifier
in the [Common Identifier Format].
* *(federation)* Added simple key server that passes the federation tester.
[@grvn-ht]: https://github.com/grvn-ht
[#181]: https://github.com/mautrix/go/pull/181
[Common Identifier Format]: https://spec.matrix.org/v1.9/appendices/#common-identifier-format
### beta.1 (2024-02-16)
* Bumped minimum Go version to 1.21.
* *(bridge)* Bumped minimum Matrix spec version to v1.4.
* **Breaking change *(crypto)*** Deleted old half-broken interactive
verification code and replaced it with a new `verificationhelper`.
* The new verification helper is still experimental.
* Both QR and emoji verification are supported (in theory).
* *(crypto)* Added support for server-side key backup.
* *(crypto)* Added support for receiving and sending secrets like cross-signing
private keys via secret sharing.
* *(crypto)* Added support for tracking which devices megolm sessions were
initially shared to, and allowing re-sharing the keys to those sessions.
* *(client)* Changed cross-signing key upload method to accept a callback for
user-interactive auth instead of only hardcoding password support.
* *(appservice)* Dropped support for legacy non-prefixed appservice paths
(e.g. `/transactions` instead of `/_matrix/app/v1/transactions`).
* *(appservice)* Dropped support for legacy `access_token` authorization in
appservice endpoints.
* *(bridge)* Fixed `RawArgs` field in command events of command state callbacks.
* *(appservice)* Added `CreateFull` helper function for creating an `AppService`
instance with all the mandatory fields set.
## 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 event source from sync event handler
function parameters to the `Mautrix.EventSource` field inside the event
struct.
* **Breaking change *(client)*** Moved `EventSource` to `event.Source`.
* *(client)* Removed deprecated `OldEventIgnorer`. The non-deprecated version
(`Client.DontProcessOldEvents`) is still available.

View File

@@ -19,8 +19,8 @@ import (
"github.com/rs/zerolog"
"go.mau.fi/util/retryafter"
"maunium.net/go/maulogger/v2/maulogadapt"
"maunium.net/go/mautrix/crypto/backup"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
@@ -34,15 +34,18 @@ type CryptoHelper interface {
Init(context.Context) error
}
// Deprecated: switch to zerolog
type Logger interface {
Debugfln(message string, args ...interface{})
}
type VerificationHelper interface {
Init(context.Context) error
StartVerification(ctx context.Context, to id.UserID) (id.VerificationTransactionID, error)
StartInRoomVerification(ctx context.Context, roomID id.RoomID, to id.UserID) (id.VerificationTransactionID, error)
AcceptVerification(ctx context.Context, txnID id.VerificationTransactionID) error
CancelVerification(ctx context.Context, txnID id.VerificationTransactionID, code event.VerificationCancelCode, reason string) error
// Deprecated: switch to zerolog
type WarnLogger interface {
Logger
Warnfln(message string, args ...interface{})
HandleScannedQRData(ctx context.Context, data []byte) error
ConfirmQRCodeScanned(ctx context.Context, txnID id.VerificationTransactionID) error
StartSAS(ctx context.Context, txnID id.VerificationTransactionID) error
ConfirmSAS(ctx context.Context, txnID id.VerificationTransactionID) error
}
// Client represents a Matrix client.
@@ -57,10 +60,9 @@ type Client struct {
Store SyncStore // The thing which can store tokens/ids
StateStore StateStore
Crypto CryptoHelper
Verification VerificationHelper
Log zerolog.Logger
// Deprecated: switch to the zerolog instance in Log
Logger Logger
RequestHook func(req *http.Request)
ResponseHook func(req *http.Request, resp *http.Response, duration time.Duration)
@@ -107,7 +109,7 @@ func DiscoverClientAPI(ctx context.Context, serverName string) (*ClientWellKnown
Path: "/.well-known/matrix/client",
}
req, err := http.NewRequestWithContext(ctx, "GET", wellKnownURL.String(), nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnownURL.String(), nil)
if err != nil {
return nil, err
}
@@ -576,14 +578,14 @@ func (cli *Client) executeCompiledRequest(req *http.Request, retries int, backof
func (cli *Client) Whoami(ctx context.Context) (resp *RespWhoami, err error) {
urlPath := cli.BuildClientURL("v3", "account", "whoami")
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, 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(ctx context.Context, filter *Filter) (resp *RespCreateFilter, err error) {
urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "filter")
_, err = cli.MakeRequest(ctx, "POST", urlPath, filter, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, filter, &resp)
return
}
@@ -764,7 +766,7 @@ func (cli *Client) RegisterDummy(ctx context.Context, req *ReqRegister) (*RespRe
// 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(ctx context.Context) (resp *RespLoginFlows, err error) {
urlPath := cli.BuildClientURL("v3", "login")
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
@@ -808,7 +810,7 @@ func (cli *Client) Login(ctx context.Context, req *ReqLogin) (resp *RespLogin, e
// This does not clear the credentials from the client instance. See ClearCredentials() instead.
func (cli *Client) Logout(ctx context.Context) (resp *RespLogout, err error) {
urlPath := cli.BuildClientURL("v3", "logout")
_, err = cli.MakeRequest(ctx, "POST", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, nil, &resp)
return
}
@@ -816,21 +818,21 @@ func (cli *Client) Logout(ctx context.Context) (resp *RespLogout, err error) {
// This does not clear the credentials from the client instance. See ClearCredentials() instead.
func (cli *Client) LogoutAll(ctx context.Context) (resp *RespLogout, err error) {
urlPath := cli.BuildClientURL("v3", "logout", "all")
_, err = cli.MakeRequest(ctx, "POST", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, 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(ctx context.Context) (resp *RespVersions, err error) {
urlPath := cli.BuildClientURL("versions")
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, 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(ctx context.Context) (resp *RespCapabilities, err error) {
urlPath := cli.BuildClientURL("v3", "capabilities")
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
@@ -847,7 +849,7 @@ func (cli *Client) JoinRoom(ctx context.Context, roomIDorAlias, serverName strin
} else {
urlPath = cli.BuildClientURL("v3", "join", roomIDorAlias)
}
_, err = cli.MakeRequest(ctx, "POST", urlPath, content, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, content, &resp)
if err == nil && cli.StateStore != nil {
err = cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin)
if err != nil {
@@ -862,7 +864,7 @@ func (cli *Client) JoinRoom(ctx context.Context, roomIDorAlias, serverName strin
// 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(ctx context.Context, roomID id.RoomID) (resp *RespJoinRoom, err error) {
_, err = cli.MakeRequest(ctx, "POST", cli.BuildClientURL("v3", "rooms", roomID, "join"), nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, cli.BuildClientURL("v3", "rooms", roomID, "join"), nil, &resp)
if err == nil && cli.StateStore != nil {
err = cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin)
if err != nil {
@@ -874,14 +876,14 @@ func (cli *Client) JoinRoomByID(ctx context.Context, roomID id.RoomID) (resp *Re
func (cli *Client) GetProfile(ctx context.Context, mxid id.UserID) (resp *RespUserProfile, err error) {
urlPath := cli.BuildClientURL("v3", "profile", mxid)
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, 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(ctx context.Context, mxid id.UserID) (resp *RespUserDisplayName, err error) {
urlPath := cli.BuildClientURL("v3", "profile", mxid, "displayname")
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
@@ -896,7 +898,7 @@ func (cli *Client) SetDisplayName(ctx context.Context, displayName string) (err
s := struct {
DisplayName string `json:"displayname"`
}{displayName}
_, err = cli.MakeRequest(ctx, "PUT", urlPath, &s, nil)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, &s, nil)
return
}
@@ -907,7 +909,7 @@ func (cli *Client) GetAvatarURL(ctx context.Context, mxid id.UserID) (url id.Con
AvatarURL id.ContentURI `json:"avatar_url"`
}{}
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &s)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &s)
if err != nil {
return
}
@@ -926,7 +928,7 @@ func (cli *Client) SetAvatarURL(ctx context.Context, url id.ContentURI) (err err
s := struct {
AvatarURL string `json:"avatar_url"`
}{url.String()}
_, err = cli.MakeRequest(ctx, "PUT", urlPath, &s, nil)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, &s, nil)
if err != nil {
return err
}
@@ -937,21 +939,21 @@ func (cli *Client) SetAvatarURL(ctx context.Context, url id.ContentURI) (err err
// BeeperUpdateProfile sets custom fields in the user's profile.
func (cli *Client) BeeperUpdateProfile(ctx context.Context, data map[string]any) (err error) {
urlPath := cli.BuildClientURL("v3", "profile", cli.UserID)
_, err = cli.MakeRequest(ctx, "PATCH", urlPath, &data, nil)
_, err = cli.MakeRequest(ctx, http.MethodPatch, 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(ctx context.Context, name string, output interface{}) (err error) {
urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "account_data", name)
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, output)
_, err = cli.MakeRequest(ctx, http.MethodGet, 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(ctx context.Context, name string, data interface{}) (err error) {
urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "account_data", name)
_, err = cli.MakeRequest(ctx, "PUT", urlPath, data, nil)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, data, nil)
if err != nil {
return err
}
@@ -962,14 +964,14 @@ func (cli *Client) SetAccountData(ctx context.Context, name string, data interfa
// 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(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(ctx, "GET", urlPath, nil, output)
_, err = cli.MakeRequest(ctx, http.MethodGet, 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(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(ctx, "PUT", urlPath, data, nil)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, data, nil)
if err != nil {
return err
}
@@ -1027,7 +1029,7 @@ func (cli *Client) SendMessageEvent(ctx context.Context, roomID id.RoomID, event
urlData := ClientURLPath{"v3", "rooms", roomID, "send", eventType.String(), txnID}
urlPath := cli.BuildURLWithQuery(urlData, queryParams)
_, err = cli.MakeRequest(ctx, "PUT", urlPath, contentJSON, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, contentJSON, &resp)
return
}
@@ -1035,7 +1037,7 @@ func (cli *Client) SendMessageEvent(ctx context.Context, roomID id.RoomID, event
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
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(ctx, "PUT", urlPath, contentJSON, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, contentJSON, &resp)
if err == nil && cli.StateStore != nil {
cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, contentJSON)
}
@@ -1048,7 +1050,7 @@ func (cli *Client) SendMassagedStateEvent(ctx context.Context, roomID id.RoomID,
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{
"ts": strconv.FormatInt(ts, 10),
})
_, err = cli.MakeRequest(ctx, "PUT", urlPath, contentJSON, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, contentJSON, &resp)
if err == nil && cli.StateStore != nil {
cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, contentJSON)
}
@@ -1102,7 +1104,7 @@ func (cli *Client) RedactEvent(ctx context.Context, roomID id.RoomID, eventID id
txnID = cli.TxnID()
}
urlPath := cli.BuildClientURL("v3", "rooms", roomID, "redact", eventID, txnID)
_, err = cli.MakeRequest(ctx, "PUT", urlPath, req.Extra, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req.Extra, &resp)
return
}
@@ -1114,7 +1116,7 @@ func (cli *Client) RedactEvent(ctx context.Context, roomID id.RoomID, eventID id
// fmt.Println("Room:", resp.RoomID)
func (cli *Client) CreateRoom(ctx context.Context, req *ReqCreateRoom) (resp *RespCreateRoom, err error) {
urlPath := cli.BuildClientURL("v3", "createRoom")
_, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
if err == nil && cli.StateStore != nil {
storeErr := cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin)
if storeErr != nil {
@@ -1153,7 +1155,7 @@ func (cli *Client) LeaveRoom(ctx context.Context, roomID id.RoomID, optionalReq
panic("invalid number of arguments to LeaveRoom")
}
u := cli.BuildClientURL("v3", "rooms", roomID, "leave")
_, err = cli.MakeRequest(ctx, "POST", u, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
if err == nil && cli.StateStore != nil {
err = cli.StateStore.SetMembership(ctx, roomID, cli.UserID, event.MembershipLeave)
if err != nil {
@@ -1166,14 +1168,14 @@ func (cli *Client) LeaveRoom(ctx context.Context, roomID id.RoomID, optionalReq
// ForgetRoom forgets a room entirely. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidforget
func (cli *Client) ForgetRoom(ctx context.Context, roomID id.RoomID) (resp *RespForgetRoom, err error) {
u := cli.BuildClientURL("v3", "rooms", roomID, "forget")
_, err = cli.MakeRequest(ctx, "POST", u, struct{}{}, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, 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(ctx context.Context, roomID id.RoomID, req *ReqInviteUser) (resp *RespInviteUser, err error) {
u := cli.BuildClientURL("v3", "rooms", roomID, "invite")
_, err = cli.MakeRequest(ctx, "POST", u, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
if err == nil && cli.StateStore != nil {
err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipInvite)
if err != nil {
@@ -1186,14 +1188,14 @@ func (cli *Client) InviteUser(ctx context.Context, roomID id.RoomID, req *ReqInv
// 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(ctx context.Context, roomID id.RoomID, req *ReqInvite3PID) (resp *RespInviteUser, err error) {
u := cli.BuildClientURL("v3", "rooms", roomID, "invite")
_, err = cli.MakeRequest(ctx, "POST", u, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, 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(ctx context.Context, roomID id.RoomID, req *ReqKickUser) (resp *RespKickUser, err error) {
u := cli.BuildClientURL("v3", "rooms", roomID, "kick")
_, err = cli.MakeRequest(ctx, "POST", u, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
if err == nil && cli.StateStore != nil {
err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipLeave)
if err != nil {
@@ -1206,7 +1208,7 @@ func (cli *Client) KickUser(ctx context.Context, roomID id.RoomID, req *ReqKickU
// BanUser bans a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban
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(ctx, "POST", u, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
if err == nil && cli.StateStore != nil {
err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipBan)
if err != nil {
@@ -1219,7 +1221,7 @@ func (cli *Client) BanUser(ctx context.Context, roomID id.RoomID, req *ReqBanUse
// UnbanUser unbans a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban
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(ctx, "POST", u, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
if err == nil && cli.StateStore != nil {
err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipLeave)
if err != nil {
@@ -1233,7 +1235,7 @@ func (cli *Client) UnbanUser(ctx context.Context, roomID id.RoomID, req *ReqUnba
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(ctx, "PUT", u, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPut, u, req, &resp)
return
}
@@ -1241,7 +1243,7 @@ func (cli *Client) UserTyping(ctx context.Context, roomID id.RoomID, typing bool
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(ctx, "GET", u, nil, resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, resp)
return
}
@@ -1253,7 +1255,7 @@ func (cli *Client) GetOwnPresence(ctx context.Context) (resp *RespPresence, err
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(ctx, "PUT", u, req, nil)
_, err = cli.MakeRequest(ctx, http.MethodPut, u, req, nil)
return
}
@@ -1295,7 +1297,7 @@ func (cli *Client) updateStoreWithOutgoingEvent(ctx context.Context, roomID id.R
// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidstateeventtypestatekey
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(ctx, "GET", u, nil, outContent)
_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, outContent)
if err == nil && cli.StateStore != nil {
cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, outContent)
}
@@ -1367,13 +1369,13 @@ func (cli *Client) State(ctx context.Context, roomID id.RoomID) (stateMap RoomSt
// GetMediaConfig fetches the configuration of the content repository, such as upload limitations.
func (cli *Client) GetMediaConfig(ctx context.Context) (resp *RespMediaConfig, err error) {
u := cli.BuildURL(MediaURLPath{"v3", "config"})
_, err = cli.MakeRequest(ctx, "GET", u, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp)
return
}
// UploadLink uploads an HTTP URL and then returns an MXC URI.
func (cli *Client) UploadLink(ctx context.Context, link string) (*RespMediaUpload, error) {
req, err := http.NewRequestWithContext(ctx, "GET", link, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, link, nil)
if err != nil {
return nil, err
}
@@ -1672,12 +1674,14 @@ func (cli *Client) GetURLPreview(ctx context.Context, url string) (*RespPreviewU
// This API is primarily designed for application services which may want to efficiently look up joined members in a room.
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(ctx, "GET", u, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp)
if err == nil && cli.StateStore != nil {
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")
if clearErr != nil {
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 {
updateErr := cli.StateStore.SetMember(ctx, roomID, userID, &event.MemberEventContent{
Membership: event.MembershipJoin,
@@ -1685,7 +1689,7 @@ func (cli *Client) JoinedMembers(ctx context.Context, roomID id.RoomID) (resp *R
Displayname: member.DisplayName,
})
if updateErr != nil {
cli.cliOrContextLog(ctx).Warn().Err(clearErr).
cli.cliOrContextLog(ctx).Warn().Err(updateErr).
Stringer("room_id", roomID).
Stringer("user_id", userID).
Msg("Failed to update membership in state store after fetching joined members")
@@ -1711,7 +1715,7 @@ func (cli *Client) Members(ctx context.Context, roomID id.RoomID, req ...ReqMemb
query["not_membership"] = string(extra.NotMembership)
}
u := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "members"}, query)
_, err = cli.MakeRequest(ctx, "GET", u, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp)
if err == nil && cli.StateStore != nil {
var clearMemberships []event.Membership
if extra.Membership != "" {
@@ -1719,9 +1723,11 @@ func (cli *Client) Members(ctx context.Context, roomID id.RoomID, req ...ReqMemb
}
if extra.NotMembership == "" {
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")
if clearErr != nil {
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(ctx, cli.StateStore, evt)
@@ -1736,7 +1742,7 @@ func (cli *Client) Members(ctx context.Context, roomID id.RoomID, req ...ReqMemb
// This API is primarily designed for application services which may want to efficiently look up joined rooms.
func (cli *Client) JoinedRooms(ctx context.Context) (resp *RespJoinedRooms, err error) {
u := cli.BuildClientURL("v3", "joined_rooms")
_, err = cli.MakeRequest(ctx, "GET", u, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp)
return
}
@@ -1775,7 +1781,7 @@ func (cli *Client) Messages(ctx context.Context, roomID id.RoomID, from, to stri
}
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "messages"}, query)
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
@@ -1810,13 +1816,13 @@ func (cli *Client) Context(ctx context.Context, roomID id.RoomID, eventID id.Eve
}
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "context", eventID}, query)
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
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(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
@@ -1837,13 +1843,13 @@ func (cli *Client) MarkReadWithContent(ctx context.Context, roomID id.RoomID, ev
// To mark a message in a specific thread as read, use pass a ReqSendReceipt as the content.
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(ctx, "POST", urlPath, content, nil)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, content, nil)
return
}
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(ctx, "POST", urlPath, content, nil)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, content, nil)
return
}
@@ -1857,7 +1863,7 @@ func (cli *Client) AddTag(ctx context.Context, roomID id.RoomID, tag string, ord
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(ctx, "PUT", urlPath, data, nil)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, data, nil)
return
}
@@ -1868,13 +1874,13 @@ func (cli *Client) GetTags(ctx context.Context, roomID id.RoomID) (tags event.Ta
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(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
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(ctx, "DELETE", urlPath, nil, nil)
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
return
}
@@ -1889,49 +1895,49 @@ func (cli *Client) SetTags(ctx context.Context, roomID id.RoomID, tags event.Tag
// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3voipturnserver
func (cli *Client) TurnServer(ctx context.Context) (resp *RespTurnServer, err error) {
urlPath := cli.BuildClientURL("v3", "voip", "turnServer")
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
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(ctx, "PUT", urlPath, &ReqAliasCreate{RoomID: roomID}, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, &ReqAliasCreate{RoomID: roomID}, &resp)
return
}
func (cli *Client) ResolveAlias(ctx context.Context, alias id.RoomAlias) (resp *RespAliasResolve, err error) {
urlPath := cli.BuildClientURL("v3", "directory", "room", alias)
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
func (cli *Client) DeleteAlias(ctx context.Context, alias id.RoomAlias) (resp *RespAliasDelete, err error) {
urlPath := cli.BuildClientURL("v3", "directory", "room", alias)
_, err = cli.MakeRequest(ctx, "DELETE", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
return
}
func (cli *Client) GetAliases(ctx context.Context, roomID id.RoomID) (resp *RespAliasList, err error) {
urlPath := cli.BuildClientURL("v3", "rooms", roomID, "aliases")
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
func (cli *Client) UploadKeys(ctx context.Context, req *ReqUploadKeys) (resp *RespUploadKeys, err error) {
urlPath := cli.BuildClientURL("v3", "keys", "upload")
_, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
return
}
func (cli *Client) QueryKeys(ctx context.Context, req *ReqQueryKeys) (resp *RespQueryKeys, err error) {
urlPath := cli.BuildClientURL("v3", "keys", "query")
_, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
return
}
func (cli *Client) ClaimKeys(ctx context.Context, req *ReqClaimKeys) (resp *RespClaimKeys, err error) {
urlPath := cli.BuildClientURL("v3", "keys", "claim")
_, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
return
}
@@ -1940,43 +1946,195 @@ func (cli *Client) GetKeyChanges(ctx context.Context, from, to string) (resp *Re
"from": from,
"to": to,
})
_, err = cli.MakeRequest(ctx, "POST", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, nil, &resp)
return
}
// GetKeyBackup retrieves the keys from the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeys
func (cli *Client) GetKeyBackup(ctx context.Context, version id.KeyBackupVersion) (resp *RespRoomKeys[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
// PutKeysInBackup stores several keys in the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeys
func (cli *Client) PutKeysInBackup(ctx context.Context, version id.KeyBackupVersion, req *ReqKeyBackup) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
return
}
// DeleteKeyBackup deletes all keys from the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeys
func (cli *Client) DeleteKeyBackup(ctx context.Context, version id.KeyBackupVersion) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
return
}
// GetKeyBackupForRoom retrieves the keys from the backup for the given room.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeysroomid
func (cli *Client) GetKeyBackupForRoom(
ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID,
) (resp *RespRoomKeyBackup[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
// PutKeysInBackupForRoom stores several keys in the backup for the given room.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeysroomid
func (cli *Client) PutKeysInBackupForRoom(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, req *ReqRoomKeyBackup) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
return
}
// DeleteKeysFromBackupForRoom deletes all the keys in the backup for the given
// room.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeysroomid
func (cli *Client) DeleteKeysFromBackupForRoom(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
return
}
// GetKeyBackupForRoomAndSession retrieves a key from the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeysroomidsessionid
func (cli *Client) GetKeyBackupForRoomAndSession(
ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, sessionID id.SessionID,
) (resp *RespKeyBackupData[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
// PutKeysInBackupForRoomAndSession stores a key in the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeysroomidsessionid
func (cli *Client) PutKeysInBackupForRoomAndSession(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, sessionID id.SessionID, req *ReqKeyBackupData) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
return
}
// DeleteKeysInBackupForRoomAndSession deletes a key from the backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeysroomidsessionid
func (cli *Client) DeleteKeysInBackupForRoomAndSession(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, sessionID id.SessionID) (resp *RespRoomKeysUpdate, err error) {
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
"version": string(version),
})
_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
return
}
// GetKeyBackupLatestVersion returns information about the latest backup version.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keysversion
func (cli *Client) GetKeyBackupLatestVersion(ctx context.Context) (resp *RespRoomKeysVersion[backup.MegolmAuthData], err error) {
urlPath := cli.BuildClientURL("v3", "room_keys", "version")
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
// CreateKeyBackupVersion creates a new key backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#post_matrixclientv3room_keysversion
func (cli *Client) CreateKeyBackupVersion(ctx context.Context, req *ReqRoomKeysVersionCreate[backup.MegolmAuthData]) (resp *RespRoomKeysVersionCreate, err error) {
urlPath := cli.BuildClientURL("v3", "room_keys", "version")
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
return
}
// GetKeyBackupVersion returns information about an existing key backup.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keysversionversion
func (cli *Client) GetKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) (resp *RespRoomKeysVersion[backup.MegolmAuthData], err error) {
urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
// UpdateKeyBackupVersion updates information about an existing key backup. Only
// the auth_data can be modified.
//
// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keysversionversion
func (cli *Client) UpdateKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion, req *ReqRoomKeysVersionUpdate[backup.MegolmAuthData]) error {
urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
_, err := cli.MakeRequest(ctx, http.MethodPut, urlPath, nil, nil)
return err
}
// DeleteKeyBackupVersion deletes an existing key backup. Both the information
// about the backup, as well as all key data related to the backup will be
// deleted.
//
// See: https://spec.matrix.org/v1.1/client-server-api/#delete_matrixclientv3room_keysversionversion
func (cli *Client) DeleteKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) error {
urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
return err
}
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(ctx, "PUT", urlPath, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
return
}
func (cli *Client) GetDevicesInfo(ctx context.Context) (resp *RespDevicesInfo, err error) {
urlPath := cli.BuildClientURL("v3", "devices")
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
func (cli *Client) GetDeviceInfo(ctx context.Context, deviceID id.DeviceID) (resp *RespDeviceInfo, err error) {
urlPath := cli.BuildClientURL("v3", "devices", deviceID)
_, err = cli.MakeRequest(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
return
}
func (cli *Client) SetDeviceInfo(ctx context.Context, deviceID id.DeviceID, req *ReqDeviceInfo) error {
urlPath := cli.BuildClientURL("v3", "devices", deviceID)
_, err := cli.MakeRequest(ctx, "PUT", urlPath, req, nil)
_, err := cli.MakeRequest(ctx, http.MethodPut, urlPath, req, nil)
return err
}
func (cli *Client) DeleteDevice(ctx context.Context, deviceID id.DeviceID, req *ReqDeleteDevice) error {
urlPath := cli.BuildClientURL("v3", "devices", deviceID)
_, err := cli.MakeRequest(ctx, "DELETE", urlPath, req, nil)
_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, req, nil)
return err
}
func (cli *Client) DeleteDevices(ctx context.Context, req *ReqDeleteDevices) error {
urlPath := cli.BuildClientURL("v3", "delete_devices")
_, err := cli.MakeRequest(ctx, "DELETE", urlPath, req, nil)
_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, req, nil)
return err
}
@@ -1992,7 +2150,7 @@ func (cli *Client) UploadCrossSigningKeys(ctx context.Context, keys *UploadCross
RequestJSON: keys,
SensitiveContent: keys.Auth != nil,
})
if respErr, ok := err.(HTTPError); ok && respErr.IsStatus(http.StatusUnauthorized) {
if respErr, ok := err.(HTTPError); ok && respErr.IsStatus(http.StatusUnauthorized) && uiaCallback != nil {
// try again with UI auth
var uiAuthResp RespUserInteractive
if err := json.Unmarshal(content, &uiAuthResp); err != nil {
@@ -2001,7 +2159,7 @@ func (cli *Client) UploadCrossSigningKeys(ctx context.Context, keys *UploadCross
auth := uiaCallback(&uiAuthResp)
if auth != nil {
keys.Auth = auth
return cli.UploadCrossSigningKeys(ctx, keys, uiaCallback)
return cli.UploadCrossSigningKeys(ctx, keys, nil)
}
}
return err
@@ -2009,7 +2167,7 @@ func (cli *Client) UploadCrossSigningKeys(ctx context.Context, keys *UploadCross
func (cli *Client) UploadSignatures(ctx context.Context, req *ReqUploadSignatures) (resp *RespUploadSignatures, err error) {
urlPath := cli.BuildClientURL("v3", "keys", "signatures", "upload")
_, err = cli.MakeRequest(ctx, "POST", urlPath, req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
return
}
@@ -2023,13 +2181,13 @@ func (cli *Client) GetScopedPushRules(ctx context.Context, scope string) (resp *
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(ctx, "GET", u.String(), nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, u.String(), nil, &resp)
return
}
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(ctx, "GET", urlPath, nil, &resp)
_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
if resp != nil {
resp.Type = kind
}
@@ -2038,7 +2196,7 @@ func (cli *Client) GetPushRule(ctx context.Context, scope string, kind pushrules
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(ctx, "DELETE", urlPath, nil, nil)
_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
return err
}
@@ -2051,7 +2209,7 @@ func (cli *Client) PutPushRule(ctx context.Context, scope string, kind pushrules
query["before"] = req.Before
}
urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "pushrules", scope, kind, ruleID}, query)
_, err := cli.MakeRequest(ctx, "PUT", urlPath, req, nil)
_, err := cli.MakeRequest(ctx, http.MethodPut, urlPath, req, nil)
return err
}
@@ -2072,7 +2230,7 @@ func (cli *Client) BatchSend(ctx context.Context, roomID id.RoomID, req *ReqBatc
if len(req.BatchID) > 0 {
query["batch_id"] = req.BatchID.String()
}
_, err = cli.MakeRequest(ctx, "POST", cli.BuildURLWithQuery(path, query), req, &resp)
_, err = cli.MakeRequest(ctx, http.MethodPost, cli.BuildURLWithQuery(path, query), req, &resp)
return
}
@@ -2123,7 +2281,7 @@ func NewClient(homeserverURL string, userID id.UserID, accessToken string) (*Cli
if err != nil {
return nil, err
}
cli := &Client{
return &Client{
AccessToken: accessToken,
UserAgent: DefaultUserAgent,
HomeserverURL: hsURL,
@@ -2135,7 +2293,5 @@ func NewClient(homeserverURL string, userID id.UserID, accessToken string) (*Cli
// The client will work with this storer: it just won't remember across restarts.
// In practice, a database backend should be used.
Store: NewMemorySyncStore(),
}
cli.Logger = maulogadapt.ZeroAsMau(&cli.Log)
return cli, nil
}, nil
}

View File

@@ -9,14 +9,16 @@ package crypto
import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
type OlmAccount struct {
Internal olm.Account
signingKey id.SigningKey
identityKey id.IdentityKey
Shared bool
Internal olm.Account
signingKey id.SigningKey
identityKey id.IdentityKey
Shared bool
KeyBackupVersion id.KeyBackupVersion
}
func NewOlmAccount() *OlmAccount {
@@ -62,11 +64,7 @@ func (account *OlmAccount) getInitialKeys(userID id.UserID, deviceID id.DeviceID
panic(err)
}
deviceKeys.Signatures = mautrix.Signatures{
userID: {
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
},
}
deviceKeys.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, deviceID.String(), signature)
return deviceKeys
}
@@ -79,11 +77,7 @@ func (account *OlmAccount) getOneTimeKeys(userID id.UserID, deviceID id.DeviceID
for keyID, key := range account.Internal.OneTimeKeys() {
key := mautrix.OneTimeKey{Key: key}
signature, _ := account.Internal.SignJSON(key)
key.Signatures = mautrix.Signatures{
userID: {
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
},
}
key.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, deviceID.String(), signature)
key.IsSigned = true
oneTimeKeys[id.NewKeyID(id.KeyAlgorithmSignedCurve25519, keyID)] = key
}

60
vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go generated vendored Normal file
View File

@@ -0,0 +1,60 @@
// Copyright (c) 2024 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 aescbc
import (
"crypto/aes"
"crypto/cipher"
"maunium.net/go/mautrix/crypto/pkcs7"
)
// Encrypt encrypts the plaintext with the key and IV. The IV length must be
// equal to the AES block size.
//
// This function might mutate the plaintext.
func Encrypt(key, iv, plaintext []byte) ([]byte, error) {
if len(key) == 0 {
return nil, ErrNoKeyProvided
}
if len(iv) != aes.BlockSize {
return nil, ErrIVNotBlockSize
}
plaintext = pkcs7.Pad(plaintext, aes.BlockSize)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cipher.NewCBCEncrypter(block, iv).CryptBlocks(plaintext, plaintext)
return plaintext, nil
}
// Decrypt decrypts the ciphertext with the key and IV. The IV length must be
// equal to the block size.
//
// This function mutates the ciphertext.
func Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
if len(key) == 0 {
return nil, ErrNoKeyProvided
}
if len(iv) != aes.BlockSize {
return nil, ErrIVNotBlockSize
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, ErrNotMultipleBlockSize
}
cipher.NewCBCDecrypter(block, iv).CryptBlocks(ciphertext, ciphertext)
return pkcs7.Unpad(ciphertext), nil
}

15
vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go generated vendored Normal file
View File

@@ -0,0 +1,15 @@
// Copyright (c) 2024 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 aescbc
import "errors"
var (
ErrNoKeyProvided = errors.New("no key")
ErrIVNotBlockSize = errors.New("IV length does not match AES block size")
ErrNotMultipleBlockSize = errors.New("ciphertext length is not a multiple of the AES block size")
)

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2024 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 backup
import (
"bytes"
"crypto/ecdh"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"go.mau.fi/util/jsonbytes"
"golang.org/x/crypto/hkdf"
"maunium.net/go/mautrix/crypto/aescbc"
)
var ErrInvalidMAC = errors.New("invalid MAC")
// EncryptedSessionData is the encrypted session_data field of a key backup as
// defined in [Section 11.12.3.2.2 of the Spec].
//
// The type parameter T represents the format of the session data contained in
// the encrypted payload.
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
type EncryptedSessionData[T any] struct {
Ciphertext jsonbytes.UnpaddedBytes `json:"ciphertext"`
Ephemeral EphemeralKey `json:"ephemeral"`
MAC jsonbytes.UnpaddedBytes `json:"mac"`
}
func calculateEncryptionParameters(sharedSecret []byte) (key, macKey, iv []byte, err error) {
hkdfReader := hkdf.New(sha256.New, sharedSecret, nil, nil)
encryptionParams := make([]byte, 80)
_, err = hkdfReader.Read(encryptionParams)
if err != nil {
return nil, nil, nil, err
}
return encryptionParams[:32], encryptionParams[32:64], encryptionParams[64:], nil
}
// calculateCompatMAC calculates the MAC for compatibility with Olm and
// Vodozemac which do not actually write the ciphertext when computing the MAC.
//
// Deprecated: Use [calculateMAC] instead.
func calculateCompatMAC(macKey []byte) []byte {
hash := hmac.New(sha256.New, macKey)
return hash.Sum(nil)[:8]
}
// calculateMAC calculates the MAC as described in step 5 of according to
// [Section 11.12.3.2.2] of the Spec.
//
// [Section 11.12.3.2.2]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func calculateMAC(macKey, ciphertext []byte) []byte {
hash := hmac.New(sha256.New, macKey)
_, err := hash.Write(ciphertext)
if err != nil {
panic(err)
}
return hash.Sum(nil)[:8]
}
// EncryptSessionData encrypts the given session data with the given recovery
// key as defined in [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func EncryptSessionData[T any](backupKey *MegolmBackupKey, sessionData T) (*EncryptedSessionData[T], error) {
sessionJSON, err := json.Marshal(sessionData)
if err != nil {
return nil, err
}
ephemeralKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
sharedSecret, err := ephemeralKey.ECDH(backupKey.PublicKey())
if err != nil {
return nil, err
}
key, macKey, iv, err := calculateEncryptionParameters(sharedSecret)
if err != nil {
return nil, err
}
ciphertext, err := aescbc.Encrypt(key, iv, sessionJSON)
if err != nil {
return nil, err
}
return &EncryptedSessionData[T]{
Ciphertext: ciphertext,
Ephemeral: EphemeralKey{ephemeralKey.PublicKey()},
MAC: calculateCompatMAC(macKey),
}, nil
}
// Decrypt decrypts the [EncryptedSessionData] into a *T using the recovery key
// by reversing the process described in [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func (esd *EncryptedSessionData[T]) Decrypt(backupKey *MegolmBackupKey) (*T, error) {
sharedSecret, err := backupKey.ECDH(esd.Ephemeral.PublicKey)
if err != nil {
return nil, err
}
key, macKey, iv, err := calculateEncryptionParameters(sharedSecret)
if err != nil {
return nil, err
}
// Verify the MAC before decrypting.
if !bytes.Equal(calculateCompatMAC(macKey), esd.MAC) {
return nil, ErrInvalidMAC
}
plaintext, err := aescbc.Decrypt(key, iv, esd.Ciphertext)
if err != nil {
return nil, err
}
var sessionData T
err = json.Unmarshal(plaintext, &sessionData)
return &sessionData, err
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2024 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 backup
import (
"crypto/ecdh"
"encoding/base64"
"encoding/json"
)
// EphemeralKey is a wrapper around an ECDH X25519 public key that implements
// JSON marshalling and unmarshalling.
type EphemeralKey struct {
*ecdh.PublicKey
}
func (k *EphemeralKey) MarshalJSON() ([]byte, error) {
if k == nil || k.PublicKey == nil {
return json.Marshal(nil)
}
return json.Marshal(base64.RawStdEncoding.EncodeToString(k.Bytes()))
}
func (k *EphemeralKey) UnmarshalJSON(data []byte) error {
var keyStr string
err := json.Unmarshal(data, &keyStr)
if err != nil {
return err
}
keyBytes, err := base64.RawStdEncoding.DecodeString(keyStr)
if err != nil {
return err
}
k.PublicKey, err = ecdh.X25519().NewPublicKey(keyBytes)
return err
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2024 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 backup
import (
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
// MegolmAuthData is the auth_data when the key backup is created with
// the [id.KeyBackupAlgorithmMegolmBackupV1] algorithm as defined in
// [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
type MegolmAuthData struct {
PublicKey id.Ed25519 `json:"public_key"`
Signatures signatures.Signatures `json:"signatures"`
}
type SenderClaimedKeys struct {
Ed25519 id.Ed25519 `json:"ed25519"`
}
// MegolmSessionData is the decrypted session_data when the key backup is created
// with the [id.KeyBackupAlgorithmMegolmBackupV1] algorithm as defined in
// [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
type MegolmSessionData struct {
Algorithm id.Algorithm `json:"algorithm"`
ForwardingKeyChain []string `json:"forwarding_curve25519_key_chain"`
SenderClaimedKeys SenderClaimedKeys `json:"sender_claimed_keys"`
SenderKey id.SenderKey `json:"sender_key"`
SessionKey string `json:"session_key"`
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2024 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 backup
import (
"crypto/ecdh"
"crypto/rand"
)
// MegolmBackupKey is a wrapper around an ECDH X25519 private key that is used
// to decrypt a megolm key backup.
type MegolmBackupKey struct {
*ecdh.PrivateKey
}
func NewMegolmBackupKey() (*MegolmBackupKey, error) {
key, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
return &MegolmBackupKey{key}, nil
}
func MegolmBackupKeyFromBytes(bytes []byte) (*MegolmBackupKey, error) {
key, err := ecdh.X25519().NewPrivateKey(bytes)
if err != nil {
return nil, err
}
return &MegolmBackupKey{key}, nil
}

View File

@@ -13,21 +13,22 @@ import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
// CrossSigningKeysCache holds the three cross-signing keys for the current user.
type CrossSigningKeysCache struct {
MasterKey *olm.PkSigning
SelfSigningKey *olm.PkSigning
UserSigningKey *olm.PkSigning
MasterKey olm.PKSigning
SelfSigningKey olm.PKSigning
UserSigningKey olm.PKSigning
}
func (cskc *CrossSigningKeysCache) PublicKeys() *CrossSigningPublicKeysCache {
return &CrossSigningPublicKeysCache{
MasterKey: cskc.MasterKey.PublicKey,
SelfSigningKey: cskc.SelfSigningKey.PublicKey,
UserSigningKey: cskc.UserSigningKey.PublicKey,
MasterKey: cskc.MasterKey.PublicKey(),
SelfSigningKey: cskc.SelfSigningKey.PublicKey(),
UserSigningKey: cskc.UserSigningKey.PublicKey(),
}
}
@@ -39,28 +40,28 @@ type CrossSigningSeeds struct {
func (mach *OlmMachine) ExportCrossSigningKeys() CrossSigningSeeds {
return CrossSigningSeeds{
MasterKey: mach.CrossSigningKeys.MasterKey.Seed,
SelfSigningKey: mach.CrossSigningKeys.SelfSigningKey.Seed,
UserSigningKey: mach.CrossSigningKeys.UserSigningKey.Seed,
MasterKey: mach.CrossSigningKeys.MasterKey.Seed(),
SelfSigningKey: mach.CrossSigningKeys.SelfSigningKey.Seed(),
UserSigningKey: mach.CrossSigningKeys.UserSigningKey.Seed(),
}
}
func (mach *OlmMachine) ImportCrossSigningKeys(keys CrossSigningSeeds) (err error) {
var keysCache CrossSigningKeysCache
if keysCache.MasterKey, err = olm.NewPkSigningFromSeed(keys.MasterKey); err != nil {
if keysCache.MasterKey, err = olm.NewPKSigningFromSeed(keys.MasterKey); err != nil {
return
}
if keysCache.SelfSigningKey, err = olm.NewPkSigningFromSeed(keys.SelfSigningKey); err != nil {
if keysCache.SelfSigningKey, err = olm.NewPKSigningFromSeed(keys.SelfSigningKey); err != nil {
return
}
if keysCache.UserSigningKey, err = olm.NewPkSigningFromSeed(keys.UserSigningKey); err != nil {
if keysCache.UserSigningKey, err = olm.NewPKSigningFromSeed(keys.UserSigningKey); err != nil {
return
}
mach.Log.Debug().
Str("master", keysCache.MasterKey.PublicKey.String()).
Str("self_signing", keysCache.SelfSigningKey.PublicKey.String()).
Str("user_signing", keysCache.UserSigningKey.PublicKey.String()).
Str("master", keysCache.MasterKey.PublicKey().String()).
Str("self_signing", keysCache.SelfSigningKey.PublicKey().String()).
Str("user_signing", keysCache.UserSigningKey.PublicKey().String()).
Msg("Imported own cross-signing keys")
mach.CrossSigningKeys = &keysCache
@@ -72,19 +73,19 @@ func (mach *OlmMachine) ImportCrossSigningKeys(keys CrossSigningSeeds) (err erro
func (mach *OlmMachine) GenerateCrossSigningKeys() (*CrossSigningKeysCache, error) {
var keysCache CrossSigningKeysCache
var err error
if keysCache.MasterKey, err = olm.NewPkSigning(); err != nil {
if keysCache.MasterKey, err = olm.NewPKSigning(); err != nil {
return nil, fmt.Errorf("failed to generate master key: %w", err)
}
if keysCache.SelfSigningKey, err = olm.NewPkSigning(); err != nil {
if keysCache.SelfSigningKey, err = olm.NewPKSigning(); err != nil {
return nil, fmt.Errorf("failed to generate self-signing key: %w", err)
}
if keysCache.UserSigningKey, err = olm.NewPkSigning(); err != nil {
if keysCache.UserSigningKey, err = olm.NewPKSigning(); err != nil {
return nil, fmt.Errorf("failed to generate user-signing key: %w", err)
}
mach.Log.Debug().
Str("master", keysCache.MasterKey.PublicKey.String()).
Str("self_signing", keysCache.SelfSigningKey.PublicKey.String()).
Str("user_signing", keysCache.UserSigningKey.PublicKey.String()).
Str("master", keysCache.MasterKey.PublicKey().String()).
Str("self_signing", keysCache.SelfSigningKey.PublicKey().String()).
Str("user_signing", keysCache.UserSigningKey.PublicKey().String()).
Msg("Generated cross-signing keys")
return &keysCache, nil
}
@@ -92,48 +93,45 @@ 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(ctx context.Context, keys *CrossSigningKeysCache, uiaCallback mautrix.UIACallback) error {
userID := mach.Client.UserID
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey.String())
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey().String())
masterKey := mautrix.CrossSigningKeys{
UserID: userID,
Usage: []id.CrossSigningUsage{id.XSUsageMaster},
Keys: map[id.KeyID]id.Ed25519{
masterKeyID: keys.MasterKey.PublicKey,
masterKeyID: keys.MasterKey.PublicKey(),
},
}
masterSig, err := mach.account.Internal.SignJSON(masterKey)
if err != nil {
return fmt.Errorf("failed to sign master key: %w", err)
}
masterKey.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, mach.Client.DeviceID.String(), masterSig)
selfKey := mautrix.CrossSigningKeys{
UserID: userID,
Usage: []id.CrossSigningUsage{id.XSUsageSelfSigning},
Keys: map[id.KeyID]id.Ed25519{
id.NewKeyID(id.KeyAlgorithmEd25519, keys.SelfSigningKey.PublicKey.String()): keys.SelfSigningKey.PublicKey,
id.NewKeyID(id.KeyAlgorithmEd25519, keys.SelfSigningKey.PublicKey().String()): keys.SelfSigningKey.PublicKey(),
},
}
selfSig, err := keys.MasterKey.SignJSON(selfKey)
if err != nil {
return fmt.Errorf("failed to sign self-signing key: %w", err)
}
selfKey.Signatures = map[id.UserID]map[id.KeyID]string{
userID: {
masterKeyID: selfSig,
},
}
selfKey.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey().String(), selfSig)
userKey := mautrix.CrossSigningKeys{
UserID: userID,
Usage: []id.CrossSigningUsage{id.XSUsageUserSigning},
Keys: map[id.KeyID]id.Ed25519{
id.NewKeyID(id.KeyAlgorithmEd25519, keys.UserSigningKey.PublicKey.String()): keys.UserSigningKey.PublicKey,
id.NewKeyID(id.KeyAlgorithmEd25519, keys.UserSigningKey.PublicKey().String()): keys.UserSigningKey.PublicKey(),
},
}
userSig, err := keys.MasterKey.SignJSON(userKey)
if err != nil {
return fmt.Errorf("failed to sign user-signing key: %w", err)
}
userKey.Signatures = map[id.UserID]map[id.KeyID]string{
userID: {
masterKeyID: userSig,
},
}
userKey.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey().String(), userSig)
err = mach.Client.UploadCrossSigningKeys(ctx, &mautrix.UploadCrossSigningKeysReq{
Master: masterKey,

View File

@@ -14,7 +14,7 @@ import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
@@ -34,31 +34,6 @@ var (
ErrMismatchingMasterKeyMAC = errors.New("mismatching cross-signing master key MAC")
)
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)
}
masterKey, ok := crossSignKeys[id.XSUsageMaster]
if !ok {
return "", ErrCrossSigningMasterKeyNotFound
}
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.Key.String())
masterKeyMAC, ok := content.Mac[masterKeyID]
if !ok {
return masterKey.Key, ErrMasterKeyMACNotFound
}
expectedMasterKeyMAC, _, err := mach.getPKAndKeysMAC(verState.sas, device.UserID, device.DeviceID,
mach.Client.UserID, mach.Client.DeviceID, transactionID, masterKey.Key, masterKeyID, content.Mac)
if err != nil {
return masterKey.Key, fmt.Errorf("failed to calculate expected MAC for master key: %w", err)
}
if masterKeyMAC != expectedMasterKeyMAC {
err = fmt.Errorf("%w: expected %s, got %s", ErrMismatchingMasterKeyMAC, expectedMasterKeyMAC, masterKeyMAC)
}
return masterKey.Key, err
}
// SignUser creates a cross-signing signature for a user, stores it and uploads it to the server.
func (mach *OlmMachine) SignUser(ctx context.Context, userID id.UserID, masterKey id.Ed25519) error {
if userID == mach.Client.UserID {
@@ -85,7 +60,7 @@ func (mach *OlmMachine) SignUser(ctx context.Context, userID id.UserID, masterKe
Str("signature", signature).
Msg("Signed master key of user with our user-signing key")
if err := mach.CryptoStore.PutSignature(ctx, 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)
}
@@ -102,7 +77,7 @@ func (mach *OlmMachine) SignOwnMasterKey(ctx context.Context) error {
userID := mach.Client.UserID
deviceID := mach.Client.DeviceID
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey()
masterKeyObj := mautrix.ReqKeysSignatures{
UserID: userID,
@@ -115,11 +90,7 @@ func (mach *OlmMachine) SignOwnMasterKey(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to sign JSON: %w", err)
}
masterKeyObj.Signatures = mautrix.Signatures{
userID: map[id.KeyID]string{
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
},
}
masterKeyObj.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, deviceID.String(), signature)
mach.Log.Debug().
Str("device_id", deviceID.String()).
Str("signature", signature).
@@ -178,7 +149,7 @@ func (mach *OlmMachine) SignOwnDevice(ctx context.Context, device *id.Device) er
Str("signature", signature).
Msg("Signed own device key with self-signing key")
if err := mach.CryptoStore.PutSignature(ctx, 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)
}
@@ -209,16 +180,12 @@ func (mach *OlmMachine) getFullDeviceKeys(ctx context.Context, device *id.Device
}
// signAndUpload signs the given key signatures object and uploads it to the server.
func (mach *OlmMachine) signAndUpload(ctx context.Context, 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)
}
req.Signatures = mautrix.Signatures{
mach.Client.UserID: map[id.KeyID]string{
id.NewKeyID(id.KeyAlgorithmEd25519, key.PublicKey.String()): signature,
},
}
req.Signatures = signatures.NewSingleSignature(mach.Client.UserID, id.KeyAlgorithmEd25519, key.PublicKey().String(), signature)
resp, err := mach.Client.UploadSignatures(ctx, &mautrix.ReqUploadSignatures{
userID: map[string]mautrix.ReqKeysSignatures{

View File

@@ -14,6 +14,7 @@ import (
"maunium.net/go/mautrix/crypto/ssss"
"maunium.net/go/mautrix/crypto/utils"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// FetchCrossSigningKeysFromSSSS fetches all the cross-signing keys from SSSS, decrypts them using the given key and stores them in the olm machine.
@@ -57,33 +58,8 @@ func (mach *OlmMachine) retrieveDecryptXSigningKey(ctx context.Context, keyName
return decryptedKey, nil
}
// GenerateAndUploadCrossSigningKeys generates a new key with all corresponding cross-signing keys.
//
// A passphrase can be provided to generate the SSSS key. If the passphrase is empty, a random key
// 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(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)
}
// generate the three cross-signing keys
keysCache, err := mach.GenerateCrossSigningKeys()
if err != nil {
return "", err
}
recoveryKey := key.RecoveryKey()
// Store the private keys in SSSS
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(ctx, keysCache, func(uiResp *mautrix.RespUserInteractive) interface{} {
func (mach *OlmMachine) GenerateAndUploadCrossSigningKeysWithPassword(ctx context.Context, userPassword, passphrase string) (string, *CrossSigningKeysCache, error) {
return mach.GenerateAndUploadCrossSigningKeys(ctx, func(uiResp *mautrix.RespUserInteractive) interface{} {
return &mautrix.ReqUIAuthLogin{
BaseAuthData: mautrix.BaseAuthData{
Type: mautrix.AuthTypePassword,
@@ -92,29 +68,68 @@ func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(ctx context.Context, u
User: mach.Client.UserID.String(),
Password: userPassword,
}
})
}, passphrase)
}
// GenerateAndUploadCrossSigningKeys generates a new key with all corresponding cross-signing keys.
//
// A passphrase can be provided to generate the SSSS key. If the passphrase is empty, a random key
// 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(ctx context.Context, uiaCallback mautrix.UIACallback, passphrase string) (string, *CrossSigningKeysCache, error) {
key, err := mach.SSSS.GenerateAndUploadKey(ctx, passphrase)
if err != nil {
return recoveryKey, fmt.Errorf("failed to publish cross-signing keys: %w", err)
return "", nil, fmt.Errorf("failed to generate and upload SSSS key: %w", err)
}
// generate the three cross-signing keys
keysCache, err := mach.GenerateCrossSigningKeys()
if err != nil {
return "", nil, err
}
// Store the private keys in SSSS
if err := mach.UploadCrossSigningKeysToSSSS(ctx, key, keysCache); err != nil {
return "", nil, fmt.Errorf("failed to upload cross-signing keys to SSSS: %w", err)
}
// Publish cross-signing keys
err = mach.PublishCrossSigningKeys(ctx, keysCache, uiaCallback)
if err != nil {
return "", nil, fmt.Errorf("failed to publish cross-signing keys: %w", err)
}
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)
return "", nil, fmt.Errorf("failed to mark %s as the default key: %w", key.ID, err)
}
return recoveryKey, nil
return key.RecoveryKey(), keysCache, nil
}
// UploadCrossSigningKeysToSSSS stores the given cross-signing keys on the server encrypted with the given key.
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 {
if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningMaster, keys.MasterKey.Seed(), key); err != nil {
return err
}
if err := mach.SSSS.SetEncryptedAccountData(ctx, 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(ctx, event.AccountDataCrossSigningUser, keys.UserSigningKey.Seed, key); err != nil {
if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningUser, keys.UserSigningKey.Seed(), key); err != nil {
return err
}
// Also store these locally
if err := mach.CryptoStore.PutCrossSigningKey(ctx, mach.Client.UserID, id.XSUsageMaster, keys.MasterKey.PublicKey()); err != nil {
return err
}
if err := mach.CryptoStore.PutCrossSigningKey(ctx, mach.Client.UserID, id.XSUsageSelfSigning, keys.SelfSigningKey.PublicKey()); err != nil {
return err
}
if err := mach.CryptoStore.PutCrossSigningKey(ctx, mach.Client.UserID, id.XSUsageUserSigning, keys.UserSigningKey.PublicKey()); err != nil {
return err
}
return nil
}

View File

@@ -11,7 +11,7 @@ import (
"context"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
@@ -80,7 +80,7 @@ func (mach *OlmMachine) storeCrossSigningKeys(ctx context.Context, crossSigningK
}
log.Debug().Msg("Verifying cross-signing key signature")
if verified, err := olm.VerifySignatureJSON(userKeys, signUserID, signKeyName, signingKey); err != nil {
if verified, err := signatures.VerifySignatureJSON(userKeys, signUserID, signKeyName, signingKey); err != nil {
log.Warn().Err(err).Msg("Error verifying cross-signing key signature")
} else {
if verified {

View File

@@ -72,7 +72,11 @@ func (mach *OlmMachine) DecryptMegolmEvent(ctx context.Context, evt *event.Event
if sess.SigningKey == ownSigningKey && sess.SenderKey == ownIdentityKey && len(sess.ForwardingChains) == 0 {
trustLevel = id.TrustStateVerified
} else {
device, err = mach.GetOrFetchDeviceByKey(ctx, evt.Sender, sess.SenderKey)
if mach.DisableDecryptKeyFetching {
device, err = mach.CryptoStore.FindDeviceByKey(ctx, evt.Sender, sess.SenderKey)
} else {
device, err = mach.GetOrFetchDeviceByKey(ctx, evt.Sender, sess.SenderKey)
}
if err != nil {
// We don't want to throw these errors as the message can still be decrypted.
log.Debug().Err(err).Msg("Failed to get device to verify session")

View File

@@ -57,7 +57,7 @@ func (mach *OlmMachine) decryptOlmEvent(ctx context.Context, evt *event.Event) (
if !ok {
return nil, NotEncryptedForMe
}
decrypted, err := mach.decryptAndParseOlmCiphertext(ctx, evt.Sender, content.SenderKey, ownContent.Type, ownContent.Body)
decrypted, err := mach.decryptAndParseOlmCiphertext(ctx, evt, content.SenderKey, ownContent.Type, ownContent.Body)
if err != nil {
return nil, err
}
@@ -69,13 +69,13 @@ type OlmEventKeys struct {
Ed25519 id.Ed25519 `json:"ed25519"`
}
func (mach *OlmMachine) decryptAndParseOlmCiphertext(ctx context.Context, sender id.UserID, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) (*DecryptedOlmEvent, error) {
func (mach *OlmMachine) decryptAndParseOlmCiphertext(ctx context.Context, evt *event.Event, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) (*DecryptedOlmEvent, error) {
if olmType != id.OlmMsgTypePreKey && olmType != id.OlmMsgTypeMsg {
return nil, UnsupportedOlmMessageType
}
endTimeTrace := mach.timeTrace(ctx, "decrypting olm ciphertext", 5*time.Second)
plaintext, err := mach.tryDecryptOlmCiphertext(ctx, sender, senderKey, olmType, ciphertext)
plaintext, err := mach.tryDecryptOlmCiphertext(ctx, evt.Sender, senderKey, olmType, ciphertext)
endTimeTrace()
if err != nil {
return nil, err
@@ -88,7 +88,8 @@ func (mach *OlmMachine) decryptAndParseOlmCiphertext(ctx context.Context, sender
if err != nil {
return nil, fmt.Errorf("failed to parse olm payload: %w", err)
}
if sender != olmEvt.Sender {
olmEvt.Type.Class = evt.Type.Class
if evt.Sender != olmEvt.Sender {
return nil, SenderMismatch
} else if mach.Client.UserID != olmEvt.Recipient {
return nil, RecipientMismatch

View File

@@ -14,7 +14,7 @@ import (
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
@@ -52,7 +52,7 @@ func (mach *OlmMachine) storeDeviceSelfSignatures(ctx context.Context, userID id
} else if _, ok := selfSigs[id.NewKeyID(id.KeyAlgorithmEd25519, pubKey.String())]; !ok {
continue
}
if verified, err := olm.VerifySignatureJSON(deviceKeys, signerUserID, pubKey.String(), pubKey); verified {
if verified, err := signatures.VerifySignatureJSON(deviceKeys, signerUserID, pubKey.String(), pubKey); verified {
if signKey, ok := deviceKeys.Keys[id.DeviceKeyID(signerKey)]; ok {
signature := deviceKeys.Signatures[signerUserID][id.NewKeyID(id.KeyAlgorithmEd25519, pubKey.String())]
log.Trace().Err(err).
@@ -245,7 +245,7 @@ func (mach *OlmMachine) validateDevice(userID id.UserID, deviceID id.DeviceID, d
return existing, fmt.Errorf("%w (expected %s, got %s)", MismatchingSigningKey, existing.SigningKey, signingKey)
}
ok, err := olm.VerifySignatureJSON(deviceKeys, userID, deviceID.String(), signingKey)
ok, err := signatures.VerifySignatureJSON(deviceKeys, userID, deviceID.String(), signingKey)
if err != nil {
return existing, fmt.Errorf("failed to verify signature: %w", err)
} else if !ok {

View File

@@ -118,7 +118,7 @@ func (mach *OlmMachine) EncryptMegolmEvent(ctx context.Context, roomID id.RoomID
log.Debug().Msg("Encrypted event successfully")
err = mach.CryptoStore.UpdateOutboundGroupSession(ctx, session)
if err != nil {
log.Warn().Err(err).Msg("Failed to update megolm session in crypto store after encrypting")
return nil, fmt.Errorf("failed to update outbound group session after encrypting: %w", err)
}
encrypted := &event.EncryptedEventContent{
Algorithm: id.AlgorithmMegolmV1,
@@ -330,6 +330,17 @@ func (mach *OlmMachine) encryptAndSendGroupSession(ctx context.Context, session
Str("target_user_id", userID.String()).
Str("target_device_id", deviceID.String()).
Msg("Encrypted group session for device")
if !mach.DisableSharedGroupSessionTracking {
err := mach.CryptoStore.MarkOutboundGroupSessionShared(ctx, userID, device.identity.IdentityKey, session.id)
if err != nil {
log.Warn().
Err(err).
Str("target_user_id", userID.String()).
Str("target_device_id", deviceID.String()).
Stringer("target_session_id", session.id).
Msg("Failed to mark outbound group session shared")
}
}
}
}

View File

@@ -12,7 +12,7 @@ import (
"fmt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@@ -109,7 +109,7 @@ func (mach *OlmMachine) createOutboundSessions(ctx context.Context, input map[id
continue
}
identity := input[userID][deviceID]
if ok, err := olm.VerifySignatureJSON(oneTimeKey.RawData, userID, deviceID.String(), identity.SigningKey); err != nil {
if ok, err := signatures.VerifySignatureJSON(oneTimeKey.RawData, userID, deviceID.String(), identity.SigningKey); err != nil {
log.Error().Err(err).Msg("Failed to verify signature of one-time key")
} else if !ok {
log.Warn().Msg("One-time key has invalid signature from device")

View File

@@ -110,12 +110,13 @@ func (a Account) IdentityKeys() (id.Ed25519, id.Curve25519) {
return ed25519, curve25519
}
// Sign returns the signature of a message using the Ed25519 key for this Account.
// Sign returns the base64-encoded 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
return []byte(base64.RawStdEncoding.EncodeToString(a.IdKeys.Ed25519.Sign(message))), nil
}
// OneTimeKeys returns the public parts of the unpublished one time keys of the Account.

View File

@@ -2,8 +2,10 @@ package cipher
import (
"bytes"
"crypto/aes"
"io"
"maunium.net/go/mautrix/crypto/aescbc"
"maunium.net/go/mautrix/crypto/goolm/crypto"
)
@@ -36,7 +38,7 @@ func deriveAESKeys(kdfInfo []byte, key []byte) (*derivedAESKeys, error) {
// AESSha512BlockSize resturns the blocksize of the cipher AESSHA256.
func AESSha512BlockSize() int {
return crypto.AESCBCBlocksize()
return aes.BlockSize
}
// AESSHA256 is a valid cipher using AES with CBC and HKDFSha256.
@@ -57,7 +59,7 @@ func (c AESSHA256) Encrypt(key, plaintext []byte) (ciphertext []byte, err error)
if err != nil {
return nil, err
}
ciphertext, err = crypto.AESCBCEncrypt(keys.key, keys.iv, plaintext)
ciphertext, err = aescbc.Encrypt(keys.key, keys.iv, plaintext)
if err != nil {
return nil, err
}
@@ -70,7 +72,7 @@ func (c AESSHA256) Decrypt(key, ciphertext []byte) (plaintext []byte, err error)
if err != nil {
return nil, err
}
plaintext, err = crypto.AESCBCDecrypt(keys.key, keys.iv, ciphertext)
plaintext, err = aescbc.Decrypt(keys.key, keys.iv, ciphertext)
if err != nil {
return nil, err
}

View File

@@ -1,4 +1,5 @@
// cipher provides the methods and structs to do encryptions for olm/megolm.
// Package cipher provides the methods and structs to do encryptions for
// olm/megolm.
package cipher
// Cipher defines a valid cipher.

View File

@@ -1,75 +0,0 @@
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)]
}

View File

@@ -0,0 +1,2 @@
// Package crpyto provides the nessesary encryption methods for olm/megolm
package crypto

View File

@@ -1,2 +0,0 @@
// crpyto provides the nessesary encryption methods for olm/megolm
package crypto

View File

@@ -21,8 +21,6 @@ var (
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?)")

View File

@@ -45,8 +45,8 @@ func NewDecryptionFromPrivate(privateKey crypto.Curve25519PrivateKey) (*Decrypti
return s, nil
}
// PubKey returns the public key base 64 encoded.
func (s Decryption) PubKey() id.Curve25519 {
// PublicKey returns the public key base 64 encoded.
func (s Decryption) PublicKey() id.Curve25519 {
return s.KeyPair.B64Encoded()
}

View File

@@ -2,7 +2,11 @@ package pk
import (
"crypto/rand"
"encoding/json"
"github.com/tidwall/sjson"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/goolm"
"maunium.net/go/mautrix/crypto/goolm/crypto"
"maunium.net/go/mautrix/id"
@@ -10,15 +14,15 @@ import (
// Signing is used for signing a pk
type Signing struct {
KeyPair crypto.Ed25519KeyPair `json:"key_pair"`
Seed []byte `json:"seed"`
keyPair crypto.Ed25519KeyPair
seed []byte
}
// 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)
s.seed = seed
s.keyPair = crypto.Ed25519GenerateFromSeed(seed)
return s, nil
}
@@ -32,13 +36,34 @@ func NewSigning() (*Signing, error) {
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)
// Seed returns the seed of the key pair.
func (s Signing) Seed() []byte {
return s.seed
}
// PublicKey returns the public key of the key pair base 64 encoded.
func (s Signing) PublicKey() id.Ed25519 {
return s.KeyPair.B64Encoded()
return s.keyPair.B64Encoded()
}
// Sign returns the signature of the message base64 encoded.
func (s Signing) Sign(message []byte) ([]byte, error) {
signature := s.keyPair.Sign(message)
return goolm.Base64Encode(signature), nil
}
// SignJSON creates a signature for the given object after encoding it to
// canonical JSON.
func (s Signing) SignJSON(obj any) (string, error) {
objJSON, err := json.Marshal(obj)
if err != nil {
return "", err
}
objJSON, _ = sjson.DeleteBytes(objJSON, "unsigned")
objJSON, _ = sjson.DeleteBytes(objJSON, "signatures")
signature, err := s.Sign(canonicaljson.CanonicalJSONAssumeValid(objJSON))
if err != nil {
return "", err
}
return string(signature), nil
}

View File

@@ -1,76 +0,0 @@
// 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)
}

View File

@@ -0,0 +1,3 @@
// Package session provides the different types of sessions for en/decrypting
// of messages
package session

View File

@@ -1,2 +0,0 @@
// session provides the different types of sessions for en/decrypting of messages
package session

View File

@@ -1,23 +0,0 @@
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
}

184
vendor/maunium.net/go/mautrix/crypto/keybackup.go generated vendored Normal file
View File

@@ -0,0 +1,184 @@
package crypto
import (
"context"
"fmt"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/backup"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
func (mach *OlmMachine) DownloadAndStoreLatestKeyBackup(ctx context.Context, megolmBackupKey *backup.MegolmBackupKey) (id.KeyBackupVersion, error) {
log := mach.machOrContextLog(ctx).With().
Str("action", "download and store latest key backup").
Logger()
ctx = log.WithContext(ctx)
versionInfo, err := mach.GetAndVerifyLatestKeyBackupVersion(ctx)
if err != nil {
return "", err
} else if versionInfo == nil {
return "", nil
}
err = mach.GetAndStoreKeyBackup(ctx, versionInfo.Version, megolmBackupKey)
return versionInfo.Version, err
}
func (mach *OlmMachine) GetAndVerifyLatestKeyBackupVersion(ctx context.Context) (*mautrix.RespRoomKeysVersion[backup.MegolmAuthData], error) {
versionInfo, err := mach.Client.GetKeyBackupLatestVersion(ctx)
if err != nil {
return nil, err
}
if versionInfo.Algorithm != id.KeyBackupAlgorithmMegolmBackupV1 {
return nil, fmt.Errorf("unsupported key backup algorithm: %s", versionInfo.Algorithm)
}
log := mach.machOrContextLog(ctx).With().
Int("count", versionInfo.Count).
Str("etag", versionInfo.ETag).
Stringer("key_backup_version", versionInfo.Version).
Logger()
userSignatures, ok := versionInfo.AuthData.Signatures[mach.Client.UserID]
if !ok {
return nil, fmt.Errorf("no signature from user %s found in key backup", mach.Client.UserID)
}
crossSigningPubkeys := mach.GetOwnCrossSigningPublicKeys(ctx)
signatureVerified := false
for keyID := range userSignatures {
keyAlg, keyName := keyID.Parse()
if keyAlg != id.KeyAlgorithmEd25519 {
continue
}
log := log.With().Str("key_name", keyName).Logger()
var key id.Ed25519
if keyName == crossSigningPubkeys.MasterKey.String() {
key = crossSigningPubkeys.MasterKey
} else if device, err := mach.GetOrFetchDevice(ctx, mach.Client.UserID, id.DeviceID(keyName)); err != nil {
log.Warn().Err(err).Msg("Failed to fetch device")
continue
} else if !mach.IsDeviceTrusted(device) {
log.Warn().Err(err).Msg("Device is not trusted")
continue
} else {
key = device.SigningKey
}
ok, err = signatures.VerifySignatureJSON(versionInfo.AuthData, mach.Client.UserID, keyName, key)
if err != nil || !ok {
log.Warn().Err(err).Stringer("key_id", keyID).Msg("Signature verification failed")
continue
} else {
// One of the signatures is valid, break from the loop.
signatureVerified = true
break
}
}
if !signatureVerified {
return nil, fmt.Errorf("no valid signature from user %s found in key backup", mach.Client.UserID)
}
return versionInfo, nil
}
func (mach *OlmMachine) GetAndStoreKeyBackup(ctx context.Context, version id.KeyBackupVersion, megolmBackupKey *backup.MegolmBackupKey) error {
keys, err := mach.Client.GetKeyBackup(ctx, version)
if err != nil {
return err
}
log := zerolog.Ctx(ctx)
var count, failedCount int
for roomID, backup := range keys.Rooms {
for sessionID, keyBackupData := range backup.Sessions {
sessionData, err := keyBackupData.SessionData.Decrypt(megolmBackupKey)
if err != nil {
log.Warn().Err(err).Msg("Failed to decrypt session data")
failedCount++
continue
}
err = mach.ImportRoomKeyFromBackup(ctx, version, roomID, sessionID, sessionData)
if err != nil {
log.Warn().Err(err).Msg("Failed to import room key from backup")
failedCount++
continue
}
count++
}
}
log.Info().
Int("count", count).
Int("failed_count", failedCount).
Msg("successfully imported sessions from backup")
return nil
}
func (mach *OlmMachine) ImportRoomKeyFromBackup(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, sessionID id.SessionID, keyBackupData *backup.MegolmSessionData) error {
log := zerolog.Ctx(ctx).With().
Str("room_id", roomID.String()).
Str("session_id", sessionID.String()).
Logger()
if keyBackupData.Algorithm != id.AlgorithmMegolmV1 {
return fmt.Errorf("ignoring room key in backup with weird algorithm %s", keyBackupData.Algorithm)
}
igsInternal, err := olm.InboundGroupSessionImport([]byte(keyBackupData.SessionKey))
if err != nil {
return fmt.Errorf("failed to import inbound group session: %w", err)
} else if igsInternal.ID() != sessionID {
log.Warn().
Stringer("actual_session_id", igsInternal.ID()).
Msg("Mismatched session ID while creating inbound group session from key backup")
return fmt.Errorf("mismatched session ID while creating inbound group session from key backup")
}
var maxAge time.Duration
var maxMessages int
if config, err := mach.StateStore.GetEncryptionEvent(ctx, roomID); err != nil {
log.Error().Err(err).Msg("Failed to get encryption event for room")
} else if config != nil {
maxAge = time.Duration(config.RotationPeriodMillis) * time.Millisecond
maxMessages = config.RotationPeriodMessages
}
if firstKnownIndex := igsInternal.FirstKnownIndex(); firstKnownIndex > 0 {
log.Warn().Uint32("first_known_index", firstKnownIndex).Msg("Importing partial session")
}
igs := &InboundGroupSession{
Internal: *igsInternal,
SigningKey: keyBackupData.SenderClaimedKeys.Ed25519,
SenderKey: keyBackupData.SenderKey,
RoomID: roomID,
ForwardingChains: append(keyBackupData.ForwardingKeyChain, keyBackupData.SenderKey.String()),
id: sessionID,
ReceivedAt: time.Now().UTC(),
MaxAge: maxAge.Milliseconds(),
MaxMessages: maxMessages,
KeyBackupVersion: version,
}
err = mach.CryptoStore.PutGroupSession(ctx, roomID, keyBackupData.SenderKey, sessionID, igs)
if err != nil {
return fmt.Errorf("failed to store new inbound group session: %w", err)
}
mach.markSessionReceived(ctx, sessionID)
return nil
}

View File

@@ -122,7 +122,7 @@ func (mach *OlmMachine) importExportedRoomKey(ctx context.Context, session Expor
if err != nil {
return false, fmt.Errorf("failed to store imported session: %w", err)
}
mach.markSessionReceived(igs.ID())
mach.markSessionReceived(ctx, igs.ID())
return true, nil
}

View File

@@ -168,6 +168,9 @@ func (mach *OlmMachine) importForwardedRoomKey(ctx context.Context, evt *Decrypt
if content.MaxMessages != 0 {
maxMessages = content.MaxMessages
}
if firstKnownIndex := igsInternal.FirstKnownIndex(); firstKnownIndex > 0 {
log.Warn().Uint32("first_known_index", firstKnownIndex).Msg("Importing partial session")
}
igs := &InboundGroupSession{
Internal: *igsInternal,
SigningKey: evt.Keys.Ed25519,
@@ -186,7 +189,7 @@ func (mach *OlmMachine) importForwardedRoomKey(ctx context.Context, evt *Decrypt
log.Error().Err(err).Msg("Failed to store new inbound group session")
return false
}
mach.markSessionReceived(content.SessionID)
mach.markSessionReceived(ctx, content.SessionID)
log.Debug().Msg("Received forwarded inbound group session")
return true
}
@@ -222,11 +225,34 @@ func (mach *OlmMachine) rejectKeyRequest(ctx context.Context, rejection KeyShare
}
}
func (mach *OlmMachine) defaultAllowKeyShare(ctx context.Context, device *id.Device, _ event.RequestedKeyInfo) *KeyShareRejection {
// sendToOneDevice sends a to-device event to a single device.
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: {
Parsed: content,
},
},
},
})
return err
}
func (mach *OlmMachine) defaultAllowKeyShare(ctx context.Context, device *id.Device, evt event.RequestedKeyInfo) *KeyShareRejection {
log := mach.machOrContextLog(ctx)
if mach.Client.UserID != device.UserID {
log.Debug().Msg("Rejecting key request from a different user")
return &KeyShareRejectOtherUser
isShared, err := mach.CryptoStore.IsOutboundGroupSessionShared(ctx, device.UserID, device.IdentityKey, evt.SessionID)
if err != nil {
log.Err(err).Msg("Rejecting key request due to internal error when checking session sharing")
return &KeyShareRejectNoResponse
} else if !isShared {
log.Debug().Msg("Rejecting key request for unshared session")
return &KeyShareRejectOtherUser
}
log.Debug().Msg("Accepting key request for shared session")
return nil
} else if mach.Client.DeviceID == device.DeviceID {
log.Debug().Msg("Ignoring key request from ourselves")
return &KeyShareRejectNoResponse
@@ -248,7 +274,7 @@ func (mach *OlmMachine) defaultAllowKeyShare(ctx context.Context, device *id.Dev
}
}
func (mach *OlmMachine) handleRoomKeyRequest(ctx context.Context, sender id.UserID, content *event.RoomKeyRequestEventContent) {
func (mach *OlmMachine) HandleRoomKeyRequest(ctx context.Context, sender id.UserID, content *event.RoomKeyRequestEventContent) {
log := zerolog.Ctx(ctx).With().
Str("request_id", content.RequestID).
Str("device_id", content.RequestingDeviceID.String()).
@@ -327,7 +353,7 @@ func (mach *OlmMachine) handleRoomKeyRequest(ctx context.Context, sender id.User
}
}
func (mach *OlmMachine) handleBeeperRoomKeyAck(ctx context.Context, sender id.UserID, content *event.BeeperRoomKeyAckEventContent) {
func (mach *OlmMachine) HandleBeeperRoomKeyAck(ctx context.Context, sender id.UserID, content *event.BeeperRoomKeyAckEventContent) {
log := mach.machOrContextLog(ctx).With().
Str("room_id", content.RoomID.String()).
Str("session_id", content.SessionID.String()).

View File

@@ -33,18 +33,17 @@ type OlmMachine struct {
PlaintextMentions bool
// Never ask the server for keys automatically as a side effect.
DisableKeyFetching bool
// Never ask the server for keys automatically as a side effect during Megolm decryption.
DisableDecryptKeyFetching bool
// Don't mark outbound Olm sessions as shared for devices they were initially sent to.
DisableSharedGroupSessionTracking bool
SendKeysMinTrust id.TrustState
ShareKeysMinTrust id.TrustState
AllowKeyShare func(context.Context, *id.Device, event.RequestedKeyInfo) *KeyShareRejection
DefaultSASTimeout time.Duration
// AcceptVerificationFrom determines whether the machine will accept verification requests from this device.
AcceptVerificationFrom func(string, *id.Device, id.RoomID) (VerificationRequestResponse, VerificationHooks)
account *OlmAccount
roomKeyRequestFilled *sync.Map
@@ -53,6 +52,9 @@ type OlmMachine struct {
keyWaiters map[id.SessionID]chan struct{}
keyWaitersLock sync.Mutex
// Optional callback which is called when we save a session to store
SessionReceived func(context.Context, id.SessionID)
devicesToUnwedge map[id.IdentityKey]bool
devicesToUnwedgeLock sync.Mutex
recentlyUnwedged map[id.IdentityKey]time.Time
@@ -78,6 +80,9 @@ type OlmMachine struct {
DeleteKeysOnDeviceDelete bool
DisableDeviceChangeKeyRotation bool
secretLock sync.Mutex
secretListeners map[string]chan<- string
}
// StateStore is used by OlmMachine to get room state information that's needed for encryption.
@@ -106,12 +111,6 @@ func NewOlmMachine(client *mautrix.Client, log *zerolog.Logger, cryptoStore Stor
SendKeysMinTrust: id.TrustStateUnset,
ShareKeysMinTrust: id.TrustStateCrossSignedTOFU,
DefaultSASTimeout: 10 * time.Minute,
AcceptVerificationFrom: func(string, *id.Device, id.RoomID) (VerificationRequestResponse, VerificationHooks) {
// Reject requests by default. Users need to override this to return appropriate verification hooks.
return RejectRequest, nil
},
roomKeyRequestFilled: &sync.Map{},
keyVerificationTransactionState: &sync.Map{},
@@ -119,6 +118,7 @@ func NewOlmMachine(client *mautrix.Client, log *zerolog.Logger, cryptoStore Stor
devicesToUnwedge: make(map[id.IdentityKey]bool),
recentlyUnwedged: make(map[id.IdentityKey]time.Time),
secretListeners: make(map[string]chan<- string),
}
mach.AllowKeyShare = mach.defaultAllowKeyShare
return mach
@@ -145,11 +145,21 @@ func (mach *OlmMachine) Load(ctx context.Context) (err error) {
return nil
}
func (mach *OlmMachine) saveAccount(ctx context.Context) {
func (mach *OlmMachine) saveAccount(ctx context.Context) error {
err := mach.CryptoStore.PutAccount(ctx, mach.account)
if err != nil {
mach.Log.Error().Err(err).Msg("Failed to save account")
}
return err
}
func (mach *OlmMachine) KeyBackupVersion() id.KeyBackupVersion {
return mach.account.KeyBackupVersion
}
func (mach *OlmMachine) SetKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) error {
mach.account.KeyBackupVersion = version
return mach.saveAccount(ctx)
}
// FlushStore calls the Flush method of the CryptoStore.
@@ -227,11 +237,7 @@ func (mach *OlmMachine) HandleDeviceLists(ctx context.Context, dl *mautrix.Devic
Str("trace_id", traceID).
Interface("changes", dl.Changed).
Msg("Device list changes in /sync")
if mach.DisableKeyFetching {
mach.CryptoStore.MarkTrackedUsersOutdated(ctx, dl.Changed)
} else {
mach.FetchKeys(ctx, dl.Changed, false)
}
mach.FetchKeys(ctx, dl.Changed, false)
mach.Log.Debug().Str("trace_id", traceID).Msg("Finished handling device list changes")
}
}
@@ -328,6 +334,47 @@ func (mach *OlmMachine) HandleMemberEvent(ctx context.Context, evt *event.Event)
}
}
func (mach *OlmMachine) HandleEncryptedEvent(ctx context.Context, evt *event.Event) {
if _, ok := evt.Content.Parsed.(*event.EncryptedEventContent); !ok {
mach.machOrContextLog(ctx).Warn().Msg("Passed invalid event to encrypted handler")
return
}
decryptedEvt, err := mach.decryptOlmEvent(ctx, evt)
if err != nil {
mach.machOrContextLog(ctx).Error().Err(err).Msg("Failed to decrypt to-device event")
return
}
log := mach.machOrContextLog(ctx).With().
Str("decrypted_type", decryptedEvt.Type.Type).
Str("sender_device", decryptedEvt.SenderDevice.String()).
Str("sender_signing_key", decryptedEvt.Keys.Ed25519.String()).
Logger()
log.Trace().Msg("Successfully decrypted to-device event")
switch decryptedContent := decryptedEvt.Content.Parsed.(type) {
case *event.RoomKeyEventContent:
mach.receiveRoomKey(ctx, decryptedEvt, decryptedContent)
log.Trace().Msg("Handled room key event")
case *event.ForwardedRoomKeyEventContent:
if mach.importForwardedRoomKey(ctx, decryptedEvt, decryptedContent) {
if ch, ok := mach.roomKeyRequestFilled.Load(decryptedContent.SessionID); ok {
// close channel to notify listener that the key was received
close(ch.(chan struct{}))
}
}
log.Trace().Msg("Handled forwarded room key event")
case *event.DummyEventContent:
log.Debug().Msg("Received encrypted dummy event")
case *event.SecretSendEventContent:
mach.receiveSecret(ctx, decryptedEvt, decryptedContent)
log.Trace().Msg("Handled secret send event")
default:
log.Debug().Msg("Unhandled encrypted to-device event")
}
}
// 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(ctx context.Context, evt *event.Event) {
@@ -352,60 +399,19 @@ func (mach *OlmMachine) HandleToDeviceEvent(ctx context.Context, evt *event.Even
}
switch content := evt.Content.Parsed.(type) {
case *event.EncryptedEventContent:
log = log.With().
Str("sender_key", content.SenderKey.String()).
Logger()
log.Debug().Msg("Handling encrypted to-device event")
ctx = log.WithContext(ctx)
decryptedEvt, err := mach.decryptOlmEvent(ctx, evt)
if err != nil {
log.Error().Err(err).Msg("Failed to decrypt to-device event")
return
}
log = log.With().
Str("decrypted_type", decryptedEvt.Type.Type).
Str("sender_device", decryptedEvt.SenderDevice.String()).
Str("sender_signing_key", decryptedEvt.Keys.Ed25519.String()).
Logger()
log.Trace().Msg("Successfully decrypted to-device event")
switch decryptedContent := decryptedEvt.Content.Parsed.(type) {
case *event.RoomKeyEventContent:
mach.receiveRoomKey(ctx, decryptedEvt, decryptedContent)
log.Trace().Msg("Handled room key event")
case *event.ForwardedRoomKeyEventContent:
if mach.importForwardedRoomKey(ctx, decryptedEvt, decryptedContent) {
if ch, ok := mach.roomKeyRequestFilled.Load(decryptedContent.SessionID); ok {
// close channel to notify listener that the key was received
close(ch.(chan struct{}))
}
}
log.Trace().Msg("Handled forwarded room key event")
case *event.DummyEventContent:
log.Debug().Msg("Received encrypted dummy event")
default:
log.Debug().Msg("Unhandled encrypted to-device event")
}
mach.HandleEncryptedEvent(ctx, evt)
return
case *event.RoomKeyRequestEventContent:
go mach.handleRoomKeyRequest(ctx, evt.Sender, content)
go mach.HandleRoomKeyRequest(ctx, evt.Sender, content)
case *event.BeeperRoomKeyAckEventContent:
mach.handleBeeperRoomKeyAck(ctx, evt.Sender, content)
// verification cases
case *event.VerificationStartEventContent:
mach.handleVerificationStart(ctx, evt.Sender, content, content.TransactionID, 10*time.Minute, "")
case *event.VerificationAcceptEventContent:
mach.handleVerificationAccept(ctx, evt.Sender, content, content.TransactionID)
case *event.VerificationKeyEventContent:
mach.handleVerificationKey(ctx, evt.Sender, content, content.TransactionID)
case *event.VerificationMacEventContent:
mach.handleVerificationMAC(ctx, evt.Sender, content, content.TransactionID)
case *event.VerificationCancelEventContent:
mach.handleVerificationCancel(evt.Sender, content, content.TransactionID)
case *event.VerificationRequestEventContent:
mach.handleVerificationRequest(ctx, evt.Sender, content, content.TransactionID, "")
mach.HandleBeeperRoomKeyAck(ctx, evt.Sender, content)
case *event.RoomKeyWithheldEventContent:
mach.handleRoomKeyWithheld(ctx, content)
mach.HandleRoomKeyWithheld(ctx, content)
case *event.SecretRequestEventContent:
if content.Action == event.SecretRequestRequest {
mach.HandleSecretRequest(ctx, evt.Sender, content)
log.Trace().Msg("Handled secret request event")
}
default:
deviceID, _ := evt.Content.Raw["device_id"].(string)
log.Debug().Str("maybe_device_id", deviceID).Msg("Unhandled to-device event")
@@ -420,7 +426,7 @@ func (mach *OlmMachine) GetOrFetchDevice(ctx context.Context, userID id.UserID,
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 || mach.DisableKeyFetching {
} else if device != nil {
return device, nil
}
if usersToDevices, err := mach.FetchKeys(ctx, []id.UserID{userID}, true); err != nil {
@@ -439,7 +445,7 @@ func (mach *OlmMachine) GetOrFetchDevice(ctx context.Context, userID id.UserID,
// 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(ctx, userID, identityKey)
if err != nil || deviceIdentity != nil || mach.DisableKeyFetching {
if err != nil || deviceIdentity != nil {
return deviceIdentity, err
}
mach.machOrContextLog(ctx).Debug().
@@ -517,7 +523,7 @@ func (mach *OlmMachine) createGroupSession(ctx context.Context, senderKey id.Sen
log.Error().Err(err).Str("session_id", sessionID.String()).Msg("Failed to store new inbound group session")
return
}
mach.markSessionReceived(sessionID)
mach.markSessionReceived(ctx, sessionID)
log.Debug().
Str("session_id", sessionID.String()).
Str("sender_key", senderKey.String()).
@@ -527,7 +533,11 @@ func (mach *OlmMachine) createGroupSession(ctx context.Context, senderKey id.Sen
Msg("Received inbound group session")
}
func (mach *OlmMachine) markSessionReceived(id id.SessionID) {
func (mach *OlmMachine) markSessionReceived(ctx context.Context, id id.SessionID) {
if mach.SessionReceived != nil {
mach.SessionReceived(ctx, id)
}
mach.keyWaitersLock.Lock()
ch, ok := mach.keyWaiters[id]
if ok {
@@ -619,7 +629,7 @@ func (mach *OlmMachine) receiveRoomKey(ctx context.Context, evt *DecryptedOlmEve
mach.createGroupSession(ctx, evt.SenderKey, evt.Keys.Ed25519, content.RoomID, content.SessionID, content.SessionKey, maxAge, maxMessages, content.IsScheduled)
}
func (mach *OlmMachine) handleRoomKeyWithheld(ctx context.Context, content *event.RoomKeyWithheldEventContent) {
func (mach *OlmMachine) HandleRoomKeyWithheld(ctx context.Context, content *event.RoomKeyWithheldEventContent) {
if content.Algorithm != id.AlgorithmMegolmV1 {
zerolog.Ctx(ctx).Debug().Interface("content", content).Msg("Non-megolm room key withheld event")
return
@@ -682,8 +692,7 @@ func (mach *OlmMachine) ShareKeys(ctx context.Context, currentOTKCount int) erro
}
mach.lastOTKUpload = time.Now()
mach.account.Shared = true
mach.saveAccount(ctx)
return nil
return mach.saveAccount(ctx)
}
func (mach *OlmMachine) ExpiredKeyDeleteLoop(ctx context.Context) {

View File

@@ -1,71 +1,29 @@
// Copyright (c) 2024 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/.
// When the goolm build flag is enabled, this file will make [PKSigning]
// constructors use the goolm constuctors.
//go:build goolm
package olm
import (
"encoding/json"
import "maunium.net/go/mautrix/crypto/goolm/pk"
"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
// NewPKSigningFromSeed creates a new PKSigning object using the given seed.
func NewPKSigningFromSeed(seed []byte) (PKSigning, error) {
return pk.NewSigningFromSeed(seed)
}
// Clear clears the underlying memory of a PkSigning object.
func (p *PkSigning) Clear() {
p.Signing = pk.Signing{}
// NewPKSigning creates a new [PKSigning] object, containing a key pair for
// signing messages.
func NewPKSigning() (PKSigning, error) {
return pk.NewSigning()
}
// 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
func NewPKDecryption(privateKey []byte) (PKDecryption, error) {
return pk.NewDecryption()
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2024 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 olm
import (
"maunium.net/go/mautrix/crypto/goolm/pk"
"maunium.net/go/mautrix/id"
)
// PKSigning is an interface for signing messages.
type PKSigning interface {
// Seed returns the seed of the key.
Seed() []byte
// PublicKey returns the public key.
PublicKey() id.Ed25519
// Sign creates a signature for the given message using this key.
Sign(message []byte) ([]byte, error)
// SignJSON creates a signature for the given object after encoding it to
// canonical JSON.
SignJSON(obj any) (string, error)
}
var _ PKSigning = (*pk.Signing)(nil)
// PKDecryption is an interface for decrypting messages.
type PKDecryption interface {
// PublicKey returns the public key.
PublicKey() id.Curve25519
// Decrypt verifies and decrypts the given message.
Decrypt(ciphertext, mac []byte, key id.Curve25519) ([]byte, error)
}
var _ PKDecryption = (*pk.Decryption)(nil)

View File

@@ -1,3 +1,9 @@
// Copyright (c) 2024 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/.
//go:build !goolm
package olm
@@ -18,14 +24,17 @@ import (
"maunium.net/go/mautrix/id"
)
// PkSigning stores a key pair for signing messages.
type PkSigning struct {
// LibOlmPKSigning stores a key pair for signing messages.
type LibOlmPKSigning struct {
int *C.OlmPkSigning
mem []byte
PublicKey id.Ed25519
Seed []byte
publicKey id.Ed25519
seed []byte
}
// Ensure that LibOlmPKSigning implements PKSigning.
var _ PKSigning = (*LibOlmPKSigning)(nil)
func pkSigningSize() uint {
return uint(C.olm_pk_signing_size())
}
@@ -42,48 +51,57 @@ func pkSigningSignatureLength() uint {
return uint(C.olm_pk_signature_length())
}
func NewBlankPkSigning() *PkSigning {
func newBlankPKSigning() *LibOlmPKSigning {
memory := make([]byte, pkSigningSize())
return &PkSigning{
return &LibOlmPKSigning{
int: C.olm_pk_signing(unsafe.Pointer(&memory[0])),
mem: memory,
}
}
// Clear clears the underlying memory of a PkSigning object.
func (p *PkSigning) Clear() {
C.olm_clear_pk_signing((*C.OlmPkSigning)(p.int))
}
// NewPkSigningFromSeed creates a new PkSigning object using the given seed.
func NewPkSigningFromSeed(seed []byte) (*PkSigning, error) {
p := NewBlankPkSigning()
p.Clear()
// NewPKSigningFromSeed creates a new [PKSigning] object using the given seed.
func NewPKSigningFromSeed(seed []byte) (PKSigning, error) {
p := newBlankPKSigning()
p.clear()
pubKey := make([]byte, pkSigningPublicKeyLength())
if C.olm_pk_signing_key_from_seed((*C.OlmPkSigning)(p.int),
unsafe.Pointer(&pubKey[0]), C.size_t(len(pubKey)),
unsafe.Pointer(&seed[0]), C.size_t(len(seed))) == errorVal() {
return nil, p.lastError()
}
p.PublicKey = id.Ed25519(pubKey)
p.Seed = seed
p.publicKey = id.Ed25519(pubKey)
p.seed = seed
return p, nil
}
// NewPkSigning creates a new PkSigning object, containing a key pair for signing messages.
func NewPkSigning() (*PkSigning, error) {
// NewPKSigning creates a new LibOlmPKSigning object, containing a key pair for
// signing messages.
func NewPKSigning() (PKSigning, error) {
// Generate the seed
seed := make([]byte, pkSigningSeedLength())
_, err := rand.Read(seed)
if err != nil {
panic(NotEnoughGoRandom)
}
pk, err := NewPkSigningFromSeed(seed)
pk, err := NewPKSigningFromSeed(seed)
return pk, err
}
func (p *LibOlmPKSigning) PublicKey() id.Ed25519 {
return p.publicKey
}
func (p *LibOlmPKSigning) Seed() []byte {
return p.seed
}
// clear clears the underlying memory of a LibOlmPKSigning object.
func (p *LibOlmPKSigning) clear() {
C.olm_clear_pk_signing((*C.OlmPkSigning)(p.int))
}
// Sign creates a signature for the given message using this key.
func (p *PkSigning) Sign(message []byte) ([]byte, error) {
func (p *LibOlmPKSigning) Sign(message []byte) ([]byte, error) {
signature := make([]byte, pkSigningSignatureLength())
if C.olm_pk_sign((*C.OlmPkSigning)(p.int), (*C.uint8_t)(unsafe.Pointer(&message[0])), C.size_t(len(message)),
(*C.uint8_t)(unsafe.Pointer(&signature[0])), C.size_t(len(signature))) == errorVal() {
@@ -93,7 +111,7 @@ func (p *PkSigning) Sign(message []byte) ([]byte, error) {
}
// SignJSON creates a signature for the given object after encoding it to canonical JSON.
func (p *PkSigning) SignJSON(obj interface{}) (string, error) {
func (p *LibOlmPKSigning) SignJSON(obj interface{}) (string, error) {
objJSON, err := json.Marshal(obj)
if err != nil {
return "", err
@@ -107,12 +125,13 @@ func (p *PkSigning) SignJSON(obj interface{}) (string, error) {
return string(signature), nil
}
// lastError returns the last error that happened in relation to this PkSigning object.
func (p *PkSigning) lastError() error {
// lastError returns the last error that happened in relation to this
// LibOlmPKSigning object.
func (p *LibOlmPKSigning) lastError() error {
return convertError(C.GoString(C.olm_pk_signing_last_error((*C.OlmPkSigning)(p.int))))
}
type PkDecryption struct {
type LibOlmPKDecryption struct {
int *C.OlmPkDecryption
mem []byte
PublicKey []byte
@@ -126,13 +145,13 @@ func pkDecryptionPublicKeySize() uint {
return uint(C.olm_pk_key_length())
}
func NewPkDecryption(privateKey []byte) (*PkDecryption, error) {
func NewPkDecryption(privateKey []byte) (*LibOlmPKDecryption, error) {
memory := make([]byte, pkDecryptionSize())
p := &PkDecryption{
p := &LibOlmPKDecryption{
int: C.olm_pk_decryption(unsafe.Pointer(&memory[0])),
mem: memory,
}
p.Clear()
p.clear()
pubKey := make([]byte, pkDecryptionPublicKeySize())
if C.olm_pk_key_from_private((*C.OlmPkDecryption)(p.int),
@@ -145,7 +164,7 @@ func NewPkDecryption(privateKey []byte) (*PkDecryption, error) {
return p, nil
}
func (p *PkDecryption) Decrypt(ephemeralKey []byte, mac []byte, ciphertext []byte) ([]byte, error) {
func (p *LibOlmPKDecryption) Decrypt(ephemeralKey []byte, mac []byte, ciphertext []byte) ([]byte, error) {
maxPlaintextLength := uint(C.olm_pk_max_plaintext_length((*C.OlmPkDecryption)(p.int), C.size_t(len(ciphertext))))
plaintext := make([]byte, maxPlaintextLength)
@@ -162,11 +181,12 @@ func (p *PkDecryption) Decrypt(ephemeralKey []byte, mac []byte, ciphertext []byt
}
// Clear clears the underlying memory of a PkDecryption object.
func (p *PkDecryption) Clear() {
func (p *LibOlmPKDecryption) clear() {
C.olm_clear_pk_decryption((*C.OlmPkDecryption)(p.int))
}
// lastError returns the last error that happened in relation to this PkDecryption object.
func (p *PkDecryption) lastError() error {
// lastError returns the last error that happened in relation to this
// LibOlmPKDecryption object.
func (p *LibOlmPKDecryption) lastError() error {
return convertError(C.GoString(C.olm_pk_decryption_last_error((*C.OlmPkDecryption)(p.int))))
}

View File

@@ -1,146 +0,0 @@
//go:build !goolm
package olm
// #cgo LDFLAGS: -lolm -lstdc++
// #include <olm/olm.h>
import "C"
import (
"encoding/json"
"fmt"
"unsafe"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"go.mau.fi/util/exgjson"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/id"
)
// Utility stores the necessary state to perform hash and signature
// verification operations.
type Utility struct {
int *C.OlmUtility
mem []byte
}
// utilitySize returns the size of a utility object in bytes.
func utilitySize() uint {
return uint(C.olm_utility_size())
}
// sha256Len returns the length of the buffer needed to hold the SHA-256 hash.
func (u *Utility) sha256Len() uint {
return uint(C.olm_sha256_length((*C.OlmUtility)(u.int)))
}
// lastError returns an error describing the most recent error to happen to a
// utility.
func (u *Utility) lastError() error {
return convertError(C.GoString(C.olm_utility_last_error((*C.OlmUtility)(u.int))))
}
// Clear clears the memory used to back this utility.
func (u *Utility) Clear() error {
r := C.olm_clear_utility((*C.OlmUtility)(u.int))
if r == errorVal() {
return u.lastError()
}
return nil
}
// NewUtility creates a new utility.
func NewUtility() *Utility {
memory := make([]byte, utilitySize())
return &Utility{
int: C.olm_utility(unsafe.Pointer(&memory[0])),
mem: memory,
}
}
// 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)
}
output := make([]byte, u.sha256Len())
r := C.olm_sha256(
(*C.OlmUtility)(u.int),
unsafe.Pointer(&([]byte(input)[0])),
C.size_t(len(input)),
unsafe.Pointer(&(output[0])),
C.size_t(len(output)))
if r == errorVal() {
panic(u.lastError())
}
return string(output)
}
// 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
}
r := C.olm_ed25519_verify(
(*C.OlmUtility)(u.int),
unsafe.Pointer(&([]byte(key)[0])),
C.size_t(len(key)),
unsafe.Pointer(&([]byte(message)[0])),
C.size_t(len(message)),
unsafe.Pointer(&([]byte(signature)[0])),
C.size_t(len(signature)))
if r == errorVal() {
err = u.lastError()
if err == BadMessageMAC {
err = nil
}
} else {
ok = true
}
return ok, err
}
// 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)
}

View File

@@ -1,92 +0,0 @@
//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)
}

View File

@@ -1,142 +0,0 @@
//go:build !nosas && !goolm
package olm
// #cgo LDFLAGS: -lolm -lstdc++
// #include <olm/olm.h>
// #include <olm/sas.h>
import "C"
import (
"crypto/rand"
"unsafe"
)
// SAS stores an Olm Short Authentication String (SAS) object.
type SAS struct {
int *C.OlmSAS
mem []byte
}
// NewBlankSAS initializes an empty SAS object.
func NewBlankSAS() *SAS {
memory := make([]byte, sasSize())
return &SAS{
int: C.olm_sas(unsafe.Pointer(&memory[0])),
mem: memory,
}
}
// sasSize is the size of a SAS object in bytes.
func sasSize() uint {
return uint(C.olm_sas_size())
}
// sasRandomLength is the number of random bytes needed to create an SAS object.
func (sas *SAS) sasRandomLength() uint {
return uint(C.olm_create_sas_random_length(sas.int))
}
// NewSAS creates a new SAS object.
func NewSAS() *SAS {
sas := NewBlankSAS()
random := make([]byte, sas.sasRandomLength()+1)
_, err := rand.Read(random)
if err != nil {
panic(NotEnoughGoRandom)
}
r := C.olm_create_sas(
(*C.OlmSAS)(sas.int),
unsafe.Pointer(&random[0]),
C.size_t(len(random)))
if r == errorVal() {
panic(sas.lastError())
} else {
return sas
}
}
// clear clears the memory used to back an SAS object.
func (sas *SAS) clear() uint {
return uint(C.olm_clear_sas(sas.int))
}
// lastError returns the most recent error to happen to an SAS object.
func (sas *SAS) lastError() error {
return convertError(C.GoString(C.olm_sas_last_error(sas.int)))
}
// pubkeyLength is the size of a public key in bytes.
func (sas *SAS) pubkeyLength() uint {
return uint(C.olm_sas_pubkey_length((*C.OlmSAS)(sas.int)))
}
// GetPubkey gets the public key for the SAS object.
func (sas *SAS) GetPubkey() []byte {
pubkey := make([]byte, sas.pubkeyLength())
r := C.olm_sas_get_pubkey(
(*C.OlmSAS)(sas.int),
unsafe.Pointer(&pubkey[0]),
C.size_t(len(pubkey)))
if r == errorVal() {
panic(sas.lastError())
}
return pubkey
}
// SetTheirKey sets the public key of the other user.
func (sas *SAS) SetTheirKey(theirKey []byte) error {
theirKeyCopy := make([]byte, len(theirKey))
copy(theirKeyCopy, theirKey)
r := C.olm_sas_set_their_key(
(*C.OlmSAS)(sas.int),
unsafe.Pointer(&theirKeyCopy[0]),
C.size_t(len(theirKeyCopy)))
if r == errorVal() {
return sas.lastError()
}
return nil
}
// GenerateBytes generates bytes to use for the short authentication string.
func (sas *SAS) GenerateBytes(info []byte, count uint) ([]byte, error) {
infoCopy := make([]byte, len(info))
copy(infoCopy, info)
output := make([]byte, count)
r := C.olm_sas_generate_bytes(
(*C.OlmSAS)(sas.int),
unsafe.Pointer(&infoCopy[0]),
C.size_t(len(infoCopy)),
unsafe.Pointer(&output[0]),
C.size_t(len(output)))
if r == errorVal() {
return nil, sas.lastError()
}
return output, nil
}
// macLength is the size of a message authentication code generated by olm_sas_calculate_mac.
func (sas *SAS) macLength() uint {
return uint(C.olm_sas_mac_length((*C.OlmSAS)(sas.int)))
}
// CalculateMAC generates a message authentication code (MAC) based on the shared secret.
func (sas *SAS) CalculateMAC(input []byte, info []byte) ([]byte, error) {
inputCopy := make([]byte, len(input))
copy(inputCopy, input)
infoCopy := make([]byte, len(info))
copy(infoCopy, info)
mac := make([]byte, sas.macLength())
r := C.olm_sas_calculate_mac(
(*C.OlmSAS)(sas.int),
unsafe.Pointer(&inputCopy[0]),
C.size_t(len(inputCopy)),
unsafe.Pointer(&infoCopy[0]),
C.size_t(len(infoCopy)),
unsafe.Pointer(&mac[0]),
C.size_t(len(mac)))
if r == errorVal() {
return nil, sas.lastError()
}
return mac, nil
}

View File

@@ -1,23 +0,0 @@
//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,
}
}

30
vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go generated vendored Normal file
View File

@@ -0,0 +1,30 @@
// Copyright (c) 2024 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 pkcs7
import "bytes"
// Pad implements PKCS#7 padding as defined in [RFC2315]. It pads the plaintext
// to the given blockSize in the range [1, 255]. This is normally used in
// AES-CBC encryption.
//
// [RFC2315]: https://www.ietf.org/rfc/rfc2315.txt
func Pad(plaintext []byte, blockSize int) []byte {
padding := blockSize - len(plaintext)%blockSize
return append(plaintext, bytes.Repeat([]byte{byte(padding)}, padding)...)
}
// Unpad implements PKCS#7 unpadding as defined in [RFC2315]. It unpads the
// plaintext by reading the padding amount from the last byte of the plaintext.
// This is normally used in AES-CBC decryption.
//
// [RFC2315]: https://www.ietf.org/rfc/rfc2315.txt
func Unpad(plaintext []byte) []byte {
length := len(plaintext)
unpadding := int(plaintext[length-1])
return plaintext[:length-unpadding]
}

View File

@@ -105,10 +105,11 @@ type InboundGroupSession struct {
ForwardingChains []string
RatchetSafety RatchetSafety
ReceivedAt time.Time
MaxAge int64
MaxMessages int
IsScheduled bool
ReceivedAt time.Time
MaxAge int64
MaxMessages int
IsScheduled bool
KeyBackupVersion id.KeyBackupVersion
id id.SessionID
}

191
vendor/maunium.net/go/mautrix/crypto/sharing.go generated vendored Normal file
View File

@@ -0,0 +1,191 @@
// 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 crypto
import (
"context"
"time"
"go.mau.fi/util/random"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// Callback function to process a received secret.
//
// Returning true or an error will immediately return from the wait loop, returning false will continue waiting for new responses.
type SecretReceiverFunc func(string) (bool, error)
func (mach *OlmMachine) GetOrRequestSecret(ctx context.Context, name id.Secret, receiver SecretReceiverFunc, timeout time.Duration) (err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// always offer our stored secret first, if any
secret, err := mach.CryptoStore.GetSecret(ctx, name)
if err != nil {
return err
} else if secret != "" {
if ok, err := receiver(secret); ok || err != nil {
return err
}
}
requestID, secretChan := random.String(64), make(chan string, 5)
mach.secretLock.Lock()
mach.secretListeners[requestID] = secretChan
mach.secretLock.Unlock()
defer func() {
mach.secretLock.Lock()
delete(mach.secretListeners, requestID)
mach.secretLock.Unlock()
}()
// request secret from any device
err = mach.sendToOneDevice(ctx, mach.Client.UserID, id.DeviceID("*"), event.ToDeviceSecretRequest, &event.SecretRequestEventContent{
Action: event.SecretRequestRequest,
RequestID: requestID,
Name: name,
RequestingDeviceID: mach.Client.DeviceID,
})
if err != nil {
return
}
// best effort cancel request from all devices when returning
defer func() {
go mach.sendToOneDevice(context.Background(), mach.Client.UserID, id.DeviceID("*"), event.ToDeviceSecretRequest, &event.SecretRequestEventContent{
Action: event.SecretRequestCancellation,
RequestID: requestID,
RequestingDeviceID: mach.Client.DeviceID,
})
}()
for {
select {
case <-ctx.Done():
return ctx.Err()
case secret = <-secretChan:
if ok, err := receiver(secret); err != nil {
return err
} else if ok {
return mach.CryptoStore.PutSecret(ctx, name, secret)
}
}
}
}
func (mach *OlmMachine) HandleSecretRequest(ctx context.Context, userID id.UserID, content *event.SecretRequestEventContent) {
log := mach.machOrContextLog(ctx).With().
Stringer("user_id", userID).
Stringer("requesting_device_id", content.RequestingDeviceID).
Stringer("action", content.Action).
Str("request_id", content.RequestID).
Stringer("secret", content.Name).
Logger()
log.Trace().Msg("Handling secret request")
if content.Action == event.SecretRequestCancellation {
log.Trace().Msg("Secret request cancellation is unimplemented, ignoring")
return
} else if content.Action != event.SecretRequestRequest {
log.Warn().Msg("Ignoring unknown secret request action")
return
}
// immediately ignore requests from other users
if userID != mach.Client.UserID || content.RequestingDeviceID == "" {
log.Debug().Msg("Secret request was not from our own device, ignoring")
return
}
if content.RequestingDeviceID == mach.Client.DeviceID {
log.Debug().Msg("Secret request was from this device, ignoring")
return
}
keys, err := mach.CryptoStore.GetCrossSigningKeys(ctx, mach.Client.UserID)
if err != nil {
log.Err(err).Msg("Failed to get cross signing keys from crypto store")
return
}
crossSigningKey, ok := keys[id.XSUsageSelfSigning]
if !ok {
log.Warn().Msg("Couldn't find self signing key to verify requesting device")
return
}
device, err := mach.GetOrFetchDevice(ctx, mach.Client.UserID, content.RequestingDeviceID)
if err != nil {
log.Err(err).Msg("Failed to get or fetch requesting device")
return
}
verified, err := mach.CryptoStore.IsKeySignedBy(ctx, mach.Client.UserID, device.SigningKey, mach.Client.UserID, crossSigningKey.Key)
if err != nil {
log.Err(err).Msg("Failed to check if requesting device is verified")
return
}
if !verified {
log.Warn().Msg("Requesting device is not verified, ignoring request")
return
}
secret, err := mach.CryptoStore.GetSecret(ctx, content.Name)
if err != nil {
log.Err(err).Msg("Failed to get secret from store")
return
} else if secret != "" {
log.Debug().Msg("Responding to secret request")
mach.SendEncryptedToDevice(ctx, device, event.ToDeviceSecretSend, event.Content{
Parsed: event.SecretSendEventContent{
RequestID: content.RequestID,
Secret: secret,
},
})
} else {
log.Debug().Msg("No stored secret found, secret request ignored")
}
}
func (mach *OlmMachine) receiveSecret(ctx context.Context, evt *DecryptedOlmEvent, content *event.SecretSendEventContent) {
log := mach.machOrContextLog(ctx).With().
Stringer("sender", evt.Sender).
Stringer("sender_device", evt.SenderDevice).
Str("request_id", content.RequestID).
Logger()
log.Trace().Msg("Handling secret send request")
// immediately ignore secrets from other users
if evt.Sender != mach.Client.UserID {
log.Warn().Msg("Secret send was not from our own device")
return
} else if content.Secret == "" {
log.Warn().Msg("We were sent an empty secret")
return
}
mach.secretLock.Lock()
secretChan := mach.secretListeners[content.RequestID]
mach.secretLock.Unlock()
if secretChan == nil {
log.Warn().Msg("We were sent a secret we didn't request")
return
}
// secret channel is buffered and we don't want to block
// at worst we drop _some_ of the responses
select {
case secretChan <- content.Secret:
default:
}
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) 2024 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 signatures
import (
"encoding/base64"
"encoding/json"
"errors"
"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/crypto"
"maunium.net/go/mautrix/id"
)
var (
ErrEmptyInput = errors.New("empty input")
ErrSignatureNotFound = errors.New("input JSON doesn't contain signature from specified device")
)
// Signatures represents a set of signatures for some data from multiple users
// and keys.
type Signatures map[id.UserID]map[id.KeyID]string
// NewSingleSignature creates a new [Signatures] object with a single
// signature.
func NewSingleSignature(userID id.UserID, algorithm id.KeyAlgorithm, keyID string, signature string) Signatures {
return Signatures{
userID: {
id.NewKeyID(algorithm, keyID): signature,
},
}
}
// VerifySignature verifies an Ed25519 signature.
func VerifySignature(message []byte, key id.Ed25519, signature []byte) (ok bool, err error) {
if len(message) == 0 || len(key) == 0 || len(signature) == 0 {
return false, ErrEmptyInput
}
keyDecoded, err := base64.RawStdEncoding.DecodeString(key.String())
if err != nil {
return false, err
}
publicKey := crypto.Ed25519PublicKey(keyDecoded)
return publicKey.Verify(message, signature), nil
}
// VerifySignatureJSON verifies the signature in the given JSON object "obj"
// as described in [Appendix 3] of the Matrix Spec.
//
// This function is a wrapper over [Utility.VerifySignatureJSON] that creates
// and destroys the [Utility] object transparently.
//
// If the "obj" is not already a [json.RawMessage], it will re-encoded as JSON
// for the verification, so "json" tags will be honored.
//
// [Appendix 3]: https://spec.matrix.org/v1.9/appendices/#signing-json
func VerifySignatureJSON(obj any, 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, ErrSignatureNotFound
}
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 := canonicaljson.CanonicalJSONAssumeValid(objJSON)
sigBytes, err := base64.RawStdEncoding.DecodeString(sig.Str)
if err != nil {
return false, err
}
return VerifySignature(objJSONString, key, sigBytes)
}

View File

@@ -21,6 +21,7 @@ import (
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/goolm/cipher"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/sql_store_upgrade"
"maunium.net/go/mautrix/event"
@@ -124,20 +125,21 @@ func (store *SQLCryptoStore) PutAccount(ctx context.Context, account *OlmAccount
store.Account = account
bytes := account.Internal.Pickle(store.PickleKey)
_, err := store.DB.Exec(ctx, `
INSERT INTO crypto_account (device_id, shared, sync_token, account, account_id) VALUES ($1, $2, $3, $4, $5)
INSERT INTO crypto_account (device_id, shared, sync_token, account, account_id, key_backup_version) VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (account_id) DO UPDATE SET shared=excluded.shared, sync_token=excluded.sync_token,
account=excluded.account, account_id=excluded.account_id
`, store.DeviceID, account.Shared, store.SyncToken, bytes, store.AccountID)
account=excluded.account, account_id=excluded.account_id,
key_backup_version=excluded.key_backup_version
`, store.DeviceID, account.Shared, store.SyncToken, bytes, store.AccountID, account.KeyBackupVersion)
return err
}
// GetAccount retrieves an OlmAccount from the database.
func (store *SQLCryptoStore) GetAccount(ctx context.Context) (*OlmAccount, error) {
if store.Account == nil {
row := store.DB.QueryRow(ctx, "SELECT shared, sync_token, account FROM crypto_account WHERE account_id=$1", store.AccountID)
row := store.DB.QueryRow(ctx, "SELECT shared, sync_token, account, key_backup_version 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)
err := row.Scan(&acc.Shared, &store.SyncToken, &accountBytes, &acc.KeyBackupVersion)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
@@ -284,17 +286,18 @@ func (store *SQLCryptoStore) PutGroupSession(ctx context.Context, roomID id.Room
_, 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
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version, account_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (session_id, account_id) DO UPDATE
SET withheld_code=NULL, withheld_reason=NULL, sender_key=excluded.sender_key, signing_key=excluded.signing_key,
room_id=excluded.room_id, session=excluded.session, forwarding_chains=excluded.forwarding_chains,
ratchet_safety=excluded.ratchet_safety, received_at=excluded.received_at,
max_age=excluded.max_age, max_messages=excluded.max_messages, is_scheduled=excluded.is_scheduled
max_age=excluded.max_age, max_messages=excluded.max_messages, is_scheduled=excluded.is_scheduled,
key_backup_version=excluded.key_backup_version
`,
sessionID, senderKey, session.SigningKey, roomID, sessionBytes, forwardingChains,
ratchetSafety, datePtr(session.ReceivedAt), intishPtr(session.MaxAge), intishPtr(session.MaxMessages),
session.IsScheduled, store.AccountID,
session.IsScheduled, session.KeyBackupVersion, store.AccountID,
)
return err
}
@@ -306,12 +309,13 @@ func (store *SQLCryptoStore) GetGroupSession(ctx context.Context, roomID id.Room
var receivedAt sql.NullTime
var maxAge, maxMessages sql.NullInt64
var isScheduled bool
var version id.KeyBackupVersion
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
SELECT sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version
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)
).Scan(&senderKeyDB, &signingKey, &sessionBytes, &forwardingChains, &withheldCode, &withheldReason, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled, &version)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
} else if err != nil {
@@ -341,6 +345,7 @@ func (store *SQLCryptoStore) GetGroupSession(ctx context.Context, roomID id.Room
MaxAge: maxAge.Int64,
MaxMessages: int(maxMessages.Int64),
IsScheduled: isScheduled,
KeyBackupVersion: version,
}, nil
}
@@ -468,7 +473,8 @@ func (store *SQLCryptoStore) scanInboundGroupSession(rows dbutil.Scannable) (*In
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)
var version id.KeyBackupVersion
err := rows.Scan(&roomID, &senderKey, &signingKey, &sessionBytes, &forwardingChains, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled, &version)
if err != nil {
return nil, err
}
@@ -484,31 +490,35 @@ func (store *SQLCryptoStore) scanInboundGroupSession(rows dbutil.Scannable) (*In
MaxAge: maxAge.Int64,
MaxMessages: int(maxMessages.Int64),
IsScheduled: isScheduled,
KeyBackupVersion: version,
}, nil
}
func (store *SQLCryptoStore) GetGroupSessionsForRoom(ctx context.Context, roomID id.RoomID) ([]*InboundGroupSession, error) {
func (store *SQLCryptoStore) GetGroupSessionsForRoom(ctx context.Context, roomID id.RoomID) dbutil.RowIter[*InboundGroupSession] {
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
SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version
FROM crypto_megolm_inbound_session WHERE room_id=$1 AND account_id=$2 AND session IS NOT NULL`,
roomID, store.AccountID,
)
if err != nil {
return nil, err
}
return dbutil.NewRowIter(rows, store.scanInboundGroupSession).AsList()
return dbutil.NewRowIterWithError(rows, store.scanInboundGroupSession, err)
}
func (store *SQLCryptoStore) GetAllGroupSessions(ctx context.Context) ([]*InboundGroupSession, error) {
func (store *SQLCryptoStore) GetAllGroupSessions(ctx context.Context) dbutil.RowIter[*InboundGroupSession] {
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`,
SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version
FROM crypto_megolm_inbound_session WHERE account_id=$1 AND session IS NOT NULL`,
store.AccountID,
)
if err != nil {
return nil, err
}
return dbutil.NewRowIter(rows, store.scanInboundGroupSession).AsList()
return dbutil.NewRowIterWithError(rows, store.scanInboundGroupSession, err)
}
func (store *SQLCryptoStore) GetGroupSessionsWithoutKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) dbutil.RowIter[*InboundGroupSession] {
rows, err := store.DB.Query(ctx, `
SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version
FROM crypto_megolm_inbound_session WHERE account_id=$1 AND session IS NOT NULL AND key_backup_version != $2`,
store.AccountID, version,
)
return dbutil.NewRowIterWithError(rows, store.scanInboundGroupSession, err)
}
// AddOutboundGroupSession stores an outbound Megolm session, along with the information about the room and involved devices.
@@ -568,6 +578,20 @@ func (store *SQLCryptoStore) RemoveOutboundGroupSession(ctx context.Context, roo
return err
}
func (store *SQLCryptoStore) MarkOutboundGroupSessionShared(ctx context.Context, userID id.UserID, identityKey id.IdentityKey, sessionID id.SessionID) error {
_, err := store.DB.Exec(ctx, "INSERT INTO crypto_megolm_outbound_session_shared (user_id, identity_key, session_id) VALUES ($1, $2, $3)", userID, identityKey, sessionID)
return err
}
func (store *SQLCryptoStore) IsOutboundGroupSessionShared(ctx context.Context, userID id.UserID, identityKey id.IdentityKey, sessionID id.SessionID) (shared bool, err error) {
err = store.DB.QueryRow(ctx, `SELECT TRUE FROM crypto_megolm_outbound_session_shared WHERE user_id=$1 AND identity_key=$2 AND session_id=$3`,
userID, identityKey, sessionID).Scan(&shared)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
// ValidateMessageIndex returns whether the given event information match the ones stored in the database
// for the given sender key, session ID and index. If the index hasn't been stored, this will store it.
func (store *SQLCryptoStore) ValidateMessageIndex(ctx context.Context, senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) (bool, error) {
@@ -845,3 +869,32 @@ func (store *SQLCryptoStore) DropSignaturesByKey(ctx context.Context, userID id.
}
return count, nil
}
func (store *SQLCryptoStore) PutSecret(ctx context.Context, name id.Secret, value string) error {
bytes, err := cipher.Pickle(store.PickleKey, []byte(value))
if err != nil {
return err
}
_, err = store.DB.Exec(ctx, `
INSERT INTO crypto_secrets (name, secret) VALUES ($1, $2)
ON CONFLICT (name) DO UPDATE SET secret=excluded.secret
`, name, bytes)
return err
}
func (store *SQLCryptoStore) GetSecret(ctx context.Context, name id.Secret) (value string, err error) {
var bytes []byte
err = store.DB.QueryRow(ctx, `SELECT secret FROM crypto_secrets WHERE name=$1`, name).Scan(&bytes)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
} else if err != nil {
return "", err
}
bytes, err = cipher.Unpickle(store.PickleKey, bytes)
return string(bytes), err
}
func (store *SQLCryptoStore) DeleteSecret(ctx context.Context, name id.Secret) (err error) {
_, err = store.DB.Exec(ctx, "DELETE FROM crypto_secrets WHERE name=$1", name)
return
}

View File

@@ -1,10 +1,11 @@
-- v0 -> v11: Latest revision
-- v0 -> v14 (compatible with v9+): Latest revision
CREATE TABLE IF NOT EXISTS crypto_account (
account_id TEXT PRIMARY KEY,
device_id TEXT NOT NULL,
shared BOOLEAN NOT NULL,
sync_token TEXT NOT NULL,
account bytea NOT NULL
account_id TEXT PRIMARY KEY,
device_id TEXT NOT NULL,
shared BOOLEAN NOT NULL,
sync_token TEXT NOT NULL,
account bytea NOT NULL,
key_backup_version TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS crypto_message_index (
@@ -44,20 +45,21 @@ CREATE TABLE IF NOT EXISTS crypto_olm_session (
);
CREATE TABLE IF NOT EXISTS crypto_megolm_inbound_session (
account_id TEXT,
session_id CHAR(43),
sender_key CHAR(43) NOT NULL,
signing_key CHAR(43),
room_id TEXT NOT NULL,
session bytea,
forwarding_chains bytea,
withheld_code TEXT,
withheld_reason TEXT,
ratchet_safety jsonb,
received_at timestamp,
max_age BIGINT,
max_messages INTEGER,
is_scheduled BOOLEAN NOT NULL DEFAULT false,
account_id TEXT,
session_id CHAR(43),
sender_key CHAR(43) NOT NULL,
signing_key CHAR(43),
room_id TEXT NOT NULL,
session bytea,
forwarding_chains bytea,
withheld_code TEXT,
withheld_reason TEXT,
ratchet_safety jsonb,
received_at timestamp,
max_age BIGINT,
max_messages INTEGER,
is_scheduled BOOLEAN NOT NULL DEFAULT false,
key_backup_version TEXT NOT NULL DEFAULT '',
PRIMARY KEY (account_id, session_id)
);
@@ -75,6 +77,14 @@ CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session (
PRIMARY KEY (account_id, room_id)
);
CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session_shared (
user_id TEXT NOT NULL,
identity_key CHAR(43) NOT NULL,
session_id CHAR(43) NOT NULL,
PRIMARY KEY (user_id, identity_key, session_id)
);
CREATE TABLE IF NOT EXISTS crypto_cross_signing_keys (
user_id TEXT,
usage TEXT,
@@ -93,3 +103,8 @@ CREATE TABLE IF NOT EXISTS crypto_cross_signing_signatures (
signature CHAR(88) NOT NULL,
PRIMARY KEY (signed_user_id, signed_key, signer_user_id, signer_key)
);
CREATE TABLE IF NOT EXISTS crypto_secrets (
name TEXT PRIMARY KEY NOT NULL,
secret bytea NOT NULL
);

View File

@@ -0,0 +1,5 @@
-- v12 (compatible with v9+): Add crypto_secrets table
CREATE TABLE IF NOT EXISTS crypto_secrets (
name TEXT PRIMARY KEY NOT NULL,
secret bytea NOT NULL
);

View File

@@ -0,0 +1,9 @@
-- v13 (compatible with v9+): Add crypto_megolm_outbound_session_shared table
CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session_shared (
user_id TEXT NOT NULL,
identity_key CHAR(43) NOT NULL,
session_id CHAR(43) NOT NULL,
PRIMARY KEY (user_id, identity_key, session_id)
);

View File

@@ -0,0 +1,4 @@
-- v14 (compatible with v9+): Add key_backup_version column to account and igs
ALTER TABLE crypto_account ADD COLUMN key_backup_version TEXT NOT NULL DEFAULT '';
ALTER TABLE crypto_megolm_inbound_session ADD COLUMN key_backup_version TEXT NOT NULL DEFAULT '';

View File

@@ -8,6 +8,7 @@ package ssss
import (
"context"
"errors"
"fmt"
"maunium.net/go/mautrix"
@@ -34,7 +35,7 @@ func (mach *Machine) GetDefaultKeyID(ctx context.Context) (string, error) {
var data DefaultSecretStorageKeyContent
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" {
if httpErr, ok := err.(mautrix.HTTPError); ok && errors.Is(httpErr.RespError, mautrix.MNotFound) {
return "", ErrNoDefaultKeyAccountDataEvent
}
return "", fmt.Errorf("failed to get default key account data from server: %w", err)

View File

@@ -67,12 +67,12 @@ func NewKey(passphrase string) (*Key, error) {
if _, err := rand.Read(ivBytes[:]); err != nil {
return nil, fmt.Errorf("failed to get random bytes for IV: %w", err)
}
keyData.IV = base64.StdEncoding.EncodeToString(ivBytes[:])
keyData.IV = base64.RawStdEncoding.EncodeToString(ivBytes[:])
keyData.MAC = keyData.calculateHash(ssssKey)
return &Key{
Key: ssssKey,
ID: base64.StdEncoding.EncodeToString(keyIDBytes),
ID: base64.RawStdEncoding.EncodeToString(keyIDBytes),
Metadata: &keyData,
}, nil
}
@@ -87,13 +87,18 @@ func (key *Key) Encrypt(eventType string, data []byte) EncryptedKeyData {
aesKey, hmacKey := utils.DeriveKeysSHA256(key.Key, eventType)
iv := utils.GenA256CTRIV()
payload := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(payload, data)
// For some reason, keys in secret storage are base64 encoded before encrypting.
// Even more confusingly, it's a part of each key type's spec rather than the secrets spec.
// Key backup (`m.megolm_backup.v1`): https://spec.matrix.org/v1.9/client-server-api/#recovery-key
// Cross-signing (master, etc): https://spec.matrix.org/v1.9/client-server-api/#cross-signing (the very last paragraph)
// It's also not clear whether unpadded base64 is the right option, but assuming it is because everything else is unpadded.
payload := make([]byte, base64.RawStdEncoding.EncodedLen(len(data)))
base64.RawStdEncoding.Encode(payload, data)
utils.XorA256CTR(payload, aesKey, iv)
return EncryptedKeyData{
Ciphertext: base64.StdEncoding.EncodeToString(payload),
IV: base64.StdEncoding.EncodeToString(iv[:]),
Ciphertext: base64.RawStdEncoding.EncodeToString(payload),
IV: base64.RawStdEncoding.EncodeToString(iv[:]),
MAC: utils.HMACSHA256B64(payload, hmacKey),
}
}
@@ -101,10 +106,10 @@ func (key *Key) Encrypt(eventType string, data []byte) EncryptedKeyData {
// Decrypt decrypts the given encrypted data with this key.
func (key *Key) Decrypt(eventType string, data EncryptedKeyData) ([]byte, error) {
var ivBytes [utils.AESCTRIVLength]byte
decodedIV, _ := base64.StdEncoding.DecodeString(data.IV)
decodedIV, _ := base64.RawStdEncoding.DecodeString(strings.TrimRight(data.IV, "="))
copy(ivBytes[:], decodedIV)
payload, err := base64.StdEncoding.DecodeString(data.Ciphertext)
payload, err := base64.RawStdEncoding.DecodeString(strings.TrimRight(data.Ciphertext, "="))
if err != nil {
return nil, err
}
@@ -114,11 +119,11 @@ func (key *Key) Decrypt(eventType string, data EncryptedKeyData) ([]byte, error)
// compare the stored MAC with the one we calculated from the ciphertext
calcMac := utils.HMACSHA256B64(payload, hmacKey)
if strings.ReplaceAll(data.MAC, "=", "") != strings.ReplaceAll(calcMac, "=", "") {
if strings.TrimRight(data.MAC, "=") != calcMac {
return nil, ErrKeyDataMACMismatch
}
utils.XorA256CTR(payload, aesKey, ivBytes)
decryptedDecoded, err := base64.StdEncoding.DecodeString(string(payload))
decryptedDecoded, err := base64.RawStdEncoding.DecodeString(strings.TrimRight(string(payload), "="))
return decryptedDecoded, err
}

View File

@@ -19,9 +19,14 @@ import (
type KeyMetadata struct {
id string
Algorithm Algorithm `json:"algorithm"`
IV string `json:"iv"`
MAC string `json:"mac"`
Name string `json:"name"`
Algorithm Algorithm `json:"algorithm"`
// Note: as per https://spec.matrix.org/v1.9/client-server-api/#msecret_storagev1aes-hmac-sha2,
// these fields are "maybe padded" base64, so both unpadded and padded values must be supported.
IV string `json:"iv"`
MAC string `json:"mac"`
Passphrase *PassphraseMetadata `json:"passphrase,omitempty"`
}
@@ -59,7 +64,7 @@ func (kd *KeyMetadata) VerifyRecoveryKey(recoverKey string) (*Key, error) {
// VerifyKey verifies the SSSS key is valid by calculating and comparing its MAC.
func (kd *KeyMetadata) VerifyKey(key []byte) bool {
return strings.ReplaceAll(kd.MAC, "=", "") == strings.ReplaceAll(kd.calculateHash(key), "=", "")
return strings.TrimRight(kd.MAC, "=") == kd.calculateHash(key)
}
// calculateHash calculates the hash used for checking if the key is entered correctly as described
@@ -68,7 +73,7 @@ func (kd *KeyMetadata) calculateHash(key []byte) string {
aesKey, hmacKey := utils.DeriveKeysSHA256(key, "")
var ivBytes [utils.AESCTRIVLength]byte
_, _ = base64.StdEncoding.Decode(ivBytes[:], []byte(kd.IV))
_, _ = base64.RawStdEncoding.Decode(ivBytes[:], []byte(strings.TrimRight(kd.IV, "=")))
cipher := utils.XorA256CTR(make([]byte, utils.AESCTRKeyLength), aesKey, ivBytes)

View File

@@ -47,6 +47,8 @@ const (
)
type EncryptedKeyData struct {
// Note: as per https://spec.matrix.org/v1.9/client-server-api/#msecret_storagev1aes-hmac-sha2-1,
// these fields are "maybe padded" base64, so both unpadded and padded values must be supported.
Ciphertext string `json:"ciphertext"`
IV string `json:"iv"`
MAC string `json:"mac"`
@@ -72,4 +74,5 @@ func init() {
event.TypeMap[event.AccountDataCrossSigningUser] = encryptedContent
event.TypeMap[event.AccountDataSecretStorageDefaultKey] = reflect.TypeOf(&DefaultSecretStorageKeyContent{})
event.TypeMap[event.AccountDataSecretStorageKey] = reflect.TypeOf(&KeyMetadata{})
event.TypeMap[event.AccountDataMegolmBackupKey] = reflect.TypeOf(&EncryptedAccountDataEventContent{})
}

View File

@@ -12,6 +12,8 @@ import (
"sort"
"sync"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@@ -68,10 +70,12 @@ type Store interface {
// 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(context.Context, id.RoomID) ([]*InboundGroupSession, error)
GetGroupSessionsForRoom(context.Context, id.RoomID) dbutil.RowIter[*InboundGroupSession]
// 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(context.Context) ([]*InboundGroupSession, error)
GetAllGroupSessions(context.Context) dbutil.RowIter[*InboundGroupSession]
// GetGroupSessionsWithoutKeyBackupVersion gets all the inbound Megolm sessions in the store that do not match given key backup version.
GetGroupSessionsWithoutKeyBackupVersion(context.Context, id.KeyBackupVersion) dbutil.RowIter[*InboundGroupSession]
// AddOutboundGroupSession inserts the given outbound Megolm session into the store.
//
@@ -84,6 +88,10 @@ type Store interface {
GetOutboundGroupSession(context.Context, id.RoomID) (*OutboundGroupSession, error)
// RemoveOutboundGroupSession removes the stored outbound Megolm session for the given room ID.
RemoveOutboundGroupSession(context.Context, id.RoomID) error
// MarkOutboutGroupSessionShared flags that the currently known device has been shared the keys for the specified session.
MarkOutboundGroupSessionShared(context.Context, id.UserID, id.IdentityKey, id.SessionID) error
// IsOutboutGroupSessionShared checks if the specified session has been shared with the device.
IsOutboundGroupSessionShared(context.Context, id.UserID, id.IdentityKey, id.SessionID) (bool, error)
// ValidateMessageIndex validates that the given message details aren't from a replay attack.
//
@@ -123,6 +131,13 @@ type Store interface {
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(context.Context, id.UserID, id.Ed25519) (int64, error)
// PutSecret stores a named secret, replacing it if it exists already.
PutSecret(context.Context, id.Secret, string) error
// GetSecret returns a named secret.
GetSecret(context.Context, id.Secret) (string, error)
// DeleteSecret removes a named secret.
DeleteSecret(context.Context, id.Secret) error
}
type messageIndexKey struct {
@@ -148,11 +163,13 @@ type MemoryStore struct {
GroupSessions map[id.RoomID]map[id.SenderKey]map[id.SessionID]*InboundGroupSession
WithheldGroupSessions map[id.RoomID]map[id.SenderKey]map[id.SessionID]*event.RoomKeyWithheldEventContent
OutGroupSessions map[id.RoomID]*OutboundGroupSession
SharedGroupSessions map[id.UserID]map[id.IdentityKey]map[id.SessionID]struct{}
MessageIndices map[messageIndexKey]messageIndexValue
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{}
Secrets map[id.Secret]string
}
var _ Store = (*MemoryStore)(nil)
@@ -168,11 +185,13 @@ func NewMemoryStore(saveCallback func() error) *MemoryStore {
GroupSessions: make(map[id.RoomID]map[id.SenderKey]map[id.SessionID]*InboundGroupSession),
WithheldGroupSessions: make(map[id.RoomID]map[id.SenderKey]map[id.SessionID]*event.RoomKeyWithheldEventContent),
OutGroupSessions: make(map[id.RoomID]*OutboundGroupSession),
SharedGroupSessions: make(map[id.UserID]map[id.IdentityKey]map[id.SessionID]struct{}),
MessageIndices: make(map[messageIndexKey]messageIndexValue),
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{}),
Secrets: make(map[id.Secret]string),
}
}
@@ -361,12 +380,12 @@ func (gs *MemoryStore) GetWithheldGroupSession(_ context.Context, roomID id.Room
return session, nil
}
func (gs *MemoryStore) GetGroupSessionsForRoom(_ context.Context, roomID id.RoomID) ([]*InboundGroupSession, error) {
func (gs *MemoryStore) GetGroupSessionsForRoom(_ context.Context, roomID id.RoomID) dbutil.RowIter[*InboundGroupSession] {
gs.lock.Lock()
defer gs.lock.Unlock()
room, ok := gs.GroupSessions[roomID]
if !ok {
return []*InboundGroupSession{}, nil
return nil
}
var result []*InboundGroupSession
for _, sessions := range room {
@@ -374,10 +393,10 @@ func (gs *MemoryStore) GetGroupSessionsForRoom(_ context.Context, roomID id.Room
result = append(result, session)
}
}
return result, nil
return dbutil.NewSliceIter[*InboundGroupSession](result)
}
func (gs *MemoryStore) GetAllGroupSessions(_ context.Context) ([]*InboundGroupSession, error) {
func (gs *MemoryStore) GetAllGroupSessions(_ context.Context) dbutil.RowIter[*InboundGroupSession] {
gs.lock.Lock()
var result []*InboundGroupSession
for _, room := range gs.GroupSessions {
@@ -388,7 +407,23 @@ func (gs *MemoryStore) GetAllGroupSessions(_ context.Context) ([]*InboundGroupSe
}
}
gs.lock.Unlock()
return result, nil
return dbutil.NewSliceIter[*InboundGroupSession](result)
}
func (gs *MemoryStore) GetGroupSessionsWithoutKeyBackupVersion(_ context.Context, version id.KeyBackupVersion) dbutil.RowIter[*InboundGroupSession] {
gs.lock.Lock()
var result []*InboundGroupSession
for _, room := range gs.GroupSessions {
for _, sessions := range room {
for _, session := range sessions {
if session.KeyBackupVersion != version {
result = append(result, session)
}
}
}
}
gs.lock.Unlock()
return dbutil.NewSliceIter[*InboundGroupSession](result)
}
func (gs *MemoryStore) AddOutboundGroupSession(_ context.Context, session *OutboundGroupSession) error {
@@ -426,6 +461,41 @@ func (gs *MemoryStore) RemoveOutboundGroupSession(_ context.Context, roomID id.R
return nil
}
func (gs *MemoryStore) MarkOutboundGroupSessionShared(_ context.Context, userID id.UserID, identityKey id.IdentityKey, sessionID id.SessionID) error {
gs.lock.Lock()
if _, ok := gs.SharedGroupSessions[userID]; !ok {
gs.SharedGroupSessions[userID] = make(map[id.IdentityKey]map[id.SessionID]struct{})
}
identities := gs.SharedGroupSessions[userID]
if _, ok := identities[identityKey]; !ok {
identities[identityKey] = make(map[id.SessionID]struct{})
}
identities[identityKey][sessionID] = struct{}{}
gs.lock.Unlock()
return nil
}
func (gs *MemoryStore) IsOutboundGroupSessionShared(_ context.Context, userID id.UserID, identityKey id.IdentityKey, sessionID id.SessionID) (isShared bool, err error) {
gs.lock.Lock()
defer gs.lock.Unlock()
if _, ok := gs.SharedGroupSessions[userID]; !ok {
return
}
identities := gs.SharedGroupSessions[userID]
if _, ok := identities[identityKey]; !ok {
return
}
_, isShared = identities[identityKey][sessionID]
return
}
func (gs *MemoryStore) ValidateMessageIndex(_ context.Context, senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) (bool, error) {
gs.lock.Lock()
defer gs.lock.Unlock()
@@ -645,3 +715,24 @@ func (gs *MemoryStore) DropSignaturesByKey(_ context.Context, userID id.UserID,
gs.lock.RUnlock()
return count, nil
}
func (gs *MemoryStore) PutSecret(_ context.Context, name id.Secret, value string) error {
gs.lock.Lock()
gs.Secrets[name] = value
gs.lock.Unlock()
return nil
}
func (gs *MemoryStore) GetSecret(_ context.Context, name id.Secret) (value string, _ error) {
gs.lock.RLock()
value = gs.Secrets[name]
gs.lock.RUnlock()
return
}
func (gs *MemoryStore) DeleteSecret(_ context.Context, name id.Secret) error {
gs.lock.Lock()
delete(gs.Secrets, name)
gs.lock.Unlock()
return nil
}

View File

@@ -124,9 +124,9 @@ func EncodeBase58RecoveryKey(key []byte) string {
return spacedKey
}
// HMACSHA256B64 calculates the base64 of the SHA256 hmac of the input with the given key.
// HMACSHA256B64 calculates the unpadded base64 of the SHA256 hmac of the input with the given key.
func HMACSHA256B64(input []byte, hmacKey [HMACKeyLength]byte) string {
h := hmac.New(sha256.New, hmacKey[:])
h.Write(input)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
return base64.RawStdEncoding.EncodeToString(h.Sum(nil))
}

View File

@@ -1,801 +0,0 @@
// Copyright (c) 2020 Nikos Filippakis
//
// 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 crypto
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"sort"
"strconv"
"strings"
"sync"
"time"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var (
ErrUnknownUserForTransaction = errors.New("unknown user for transaction")
ErrTransactionAlreadyExists = errors.New("transaction already exists")
// ErrUnknownTransaction is returned when a key verification message is received with an unknown transaction ID.
ErrUnknownTransaction = errors.New("unknown transaction")
// ErrUnknownVerificationMethod is returned when the verification method in a received m.key.verification.start is unknown.
ErrUnknownVerificationMethod = errors.New("unknown verification method")
)
type VerificationHooks interface {
// VerifySASMatch receives the generated SAS and its method, as well as the device that is being verified.
// It returns whether the given SAS match with the SAS displayed on other device.
VerifySASMatch(otherDevice *id.Device, sas SASData) bool
// VerificationMethods returns the list of supported verification methods in order of preference.
// It must contain at least the decimal method.
VerificationMethods() []VerificationMethod
OnCancel(cancelledByUs bool, reason string, reasonCode event.VerificationCancelCode)
OnSuccess()
}
type VerificationRequestResponse int
const (
AcceptRequest VerificationRequestResponse = iota
RejectRequest
IgnoreRequest
)
// sendToOneDevice sends a to-device event to a single device.
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: {
Parsed: content,
},
},
},
})
return err
}
func (mach *OlmMachine) getPKAndKeysMAC(sas *olm.SAS, sendingUser id.UserID, sendingDevice id.DeviceID, receivingUser id.UserID, receivingDevice id.DeviceID,
transactionID string, signingKey id.SigningKey, mainKeyID id.KeyID, keys map[id.KeyID]string) (string, string, error) {
sasInfo := "MATRIX_KEY_VERIFICATION_MAC" +
sendingUser.String() + sendingDevice.String() +
receivingUser.String() + receivingDevice.String() +
transactionID
// get key IDs from key map
keyIDStrings := make([]string, len(keys))
i := 0
for keyID := range keys {
keyIDStrings[i] = keyID.String()
i++
}
sort.Sort(sort.StringSlice(keyIDStrings))
keyIDString := strings.Join(keyIDStrings, ",")
pubKeyMac, err := sas.CalculateMAC([]byte(signingKey), []byte(sasInfo+mainKeyID.String()))
if err != nil {
return "", "", err
}
mach.Log.Trace().Msgf("sas.CalculateMAC(\"%s\", \"%s\") -> \"%s\"", signingKey, sasInfo+mainKeyID.String(), string(pubKeyMac))
keysMac, err := sas.CalculateMAC([]byte(keyIDString), []byte(sasInfo+"KEY_IDS"))
if err != nil {
return "", "", err
}
mach.Log.Trace().Msgf("sas.CalculateMAC(\"%s\", \"%s\") -> \"%s\"", keyIDString, sasInfo+"KEY_IDS", string(keysMac))
return string(pubKeyMac), string(keysMac), nil
}
// verificationState holds all the information needed for the state of a SAS verification with another device.
type verificationState struct {
sas *olm.SAS
otherDevice *id.Device
initiatedByUs bool
verificationStarted bool
keyReceived bool
sasMatched chan bool
commitment string
startEventCanonical string
chosenSASMethod VerificationMethod
hooks VerificationHooks
extendTimeout context.CancelFunc
inRoomID id.RoomID
lock sync.Mutex
}
// 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(ctx context.Context, transactionID string, userID id.UserID) (*verificationState, error) {
verStateInterface, ok := mach.keyVerificationTransactionState.Load(userID.String() + ":" + transactionID)
if !ok {
_ = 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(ctx, userID, id.DeviceID("*"), transactionID, reason, event.VerificationCancelUserMismatch)
} else {
_ = 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)
}
return verState, nil
}
// 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(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(ctx, userID, content.FromDevice)
if err != nil {
mach.Log.Error().Msgf("Could not find device %v of user %v", content.FromDevice, userID)
return
}
warnAndCancel := func(logReason, cancelReason string) {
mach.Log.Warn().Msgf("Canceling verification transaction %v as it %s", transactionID, logReason)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, cancelReason, event.VerificationCancelUnknownMethod)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, cancelReason, event.VerificationCancelUnknownMethod)
}
}
switch {
case content.Method != event.VerificationMethodSAS:
warnAndCancel("is not SAS", "Only SAS method is supported")
case !content.SupportsKeyAgreementProtocol(event.KeyAgreementCurve25519HKDFSHA256):
warnAndCancel("does not support key agreement protocol curve25519-hkdf-sha256",
"Only curve25519-hkdf-sha256 key agreement protocol is supported")
case !content.SupportsHashMethod(event.VerificationHashSHA256):
warnAndCancel("does not support SHA256 hashing", "Only SHA256 hashing is supported")
case !content.SupportsMACMethod(event.HKDFHMACSHA256):
warnAndCancel("does not support MAC method hkdf-hmac-sha256", "Only hkdf-hmac-sha256 MAC method is supported")
case !content.SupportsSASMethod(event.SASDecimal):
warnAndCancel("does not support decimal SAS", "Decimal SAS method must be supported")
default:
mach.actuallyStartVerification(ctx, userID, content, otherDevice, transactionID, timeout, inRoomID)
}
}
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(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(ctx, inRoomID, otherDevice.UserID, transactionID, "Internal state error in gomuks :(", "net.maunium.internal_error")
return
}
mach.timeoutAfter(ctx, verState, transactionID, timeout)
sasMethods := commonSASMethods(verState.hooks, content.ShortAuthenticationString)
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)
}
verState.chosenSASMethod = sasMethods[0]
verState.verificationStarted = true
return
}
resp, hooks := mach.AcceptVerificationFrom(transactionID, otherDevice, inRoomID)
if resp == AcceptRequest {
sasMethods := commonSASMethods(hooks, content.ShortAuthenticationString)
if len(sasMethods) == 0 {
mach.Log.Error().Msgf("No common SAS methods: %v", content.ShortAuthenticationString)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod)
}
return
}
verState := &verificationState{
sas: olm.NewSAS(),
otherDevice: otherDevice,
initiatedByUs: false,
verificationStarted: true,
keyReceived: false,
sasMatched: make(chan bool, 1),
hooks: hooks,
chosenSASMethod: sasMethods[0],
inRoomID: inRoomID,
}
verState.lock.Lock()
defer verState.lock.Unlock()
_, loaded := mach.keyVerificationTransactionState.LoadOrStore(userID.String()+":"+transactionID, verState)
if loaded {
// transaction already exists
mach.Log.Error().Msgf("Transaction %v already exists, canceling", transactionID)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage)
}
return
}
mach.timeoutAfter(ctx, verState, transactionID, timeout)
var err error
if inRoomID == "" {
err = mach.SendSASVerificationAccept(ctx, userID, content, verState.sas.GetPubkey(), sasMethods)
} else {
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)
}
} else if resp == RejectRequest {
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(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
} else {
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)
}
} else {
mach.Log.Debug().Msgf("Ignoring SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
}
}
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
for {
<-timeoutCtx.Done()
// when timeout context is done
verState.lock.Lock()
// if transaction not active anymore, return
if _, ok := mach.keyVerificationTransactionState.Load(mapKey); !ok {
verState.lock.Unlock()
return
}
if timeoutCtx.Err() == context.DeadlineExceeded {
// if deadline exceeded cancel due to timeout
mach.keyVerificationTransactionState.Delete(mapKey)
_ = 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
}
// otherwise the cancel func was called, so the timeout is reset
mach.Log.Debug().Msgf("Extending timeout for transaction %v", transactionID)
timeoutCtx, timeoutCancel = context.WithTimeout(context.Background(), timeout)
verState.extendTimeout = timeoutCancel
verState.lock.Unlock()
}
}()
}
// 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(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(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("Error getting transaction state: %v", err)
return
}
verState.lock.Lock()
defer verState.lock.Unlock()
verState.extendTimeout()
if !verState.initiatedByUs || verState.verificationStarted {
// 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(ctx, verState, transactionID, "Unexpected accept message", event.VerificationCancelUnexpectedMessage)
return
}
sasMethods := commonSASMethods(verState.hooks, content.ShortAuthenticationString)
if content.KeyAgreementProtocol != event.KeyAgreementCurve25519HKDFSHA256 ||
content.Hash != event.VerificationHashSHA256 ||
content.MessageAuthenticationCode != event.HKDFHMACSHA256 ||
len(sasMethods) == 0 {
mach.Log.Warn().Msgf("Canceling verification transaction %v due to unknown parameter", transactionID)
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Verification uses unknown method", event.VerificationCancelUnknownMethod)
return
}
key := verState.sas.GetPubkey()
verState.commitment = content.Commitment
verState.chosenSASMethod = sasMethods[0]
verState.verificationStarted = true
if verState.inRoomID == "" {
err = mach.SendSASVerificationKey(ctx, userID, verState.otherDevice.DeviceID, transactionID, string(key))
} else {
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)
return
}
}
// 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(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(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("Error getting transaction state: %v", err)
return
}
verState.lock.Lock()
defer verState.lock.Unlock()
verState.extendTimeout()
device := verState.otherDevice
if !verState.verificationStarted || verState.keyReceived {
// 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(ctx, verState, transactionID, "Unexpected key message", event.VerificationCancelUnexpectedMessage)
return
}
if err := verState.sas.SetTheirKey([]byte(content.Key)); err != nil {
mach.Log.Error().Msgf("Error setting other device's key: %v", err)
return
}
verState.keyReceived = true
if verState.initiatedByUs {
// verify commitment string from accept message now
expectedCommitment := olm.NewUtility().Sha256(content.Key + verState.startEventCanonical)
mach.Log.Debug().Msgf("Received commitment: %v Expected: %v", verState.commitment, expectedCommitment)
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(ctx, verState, transactionID, "Commitment mismatch", event.VerificationCancelCommitmentMismatch)
return
}
} else {
// if verification was initiated by other device, send out our key now
key := verState.sas.GetPubkey()
if verState.inRoomID == "" {
err = mach.SendSASVerificationKey(ctx, userID, device.DeviceID, transactionID, string(key))
} else {
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)
return
}
}
// compare the SAS keys in a new goroutine and, when the verification is complete, send out the MAC
var initUserID, acceptUserID id.UserID
var initDeviceID, acceptDeviceID id.DeviceID
var initKey, acceptKey string
if verState.initiatedByUs {
initUserID = mach.Client.UserID
initDeviceID = mach.Client.DeviceID
initKey = string(verState.sas.GetPubkey())
acceptUserID = device.UserID
acceptDeviceID = device.DeviceID
acceptKey = content.Key
} else {
initUserID = device.UserID
initDeviceID = device.DeviceID
initKey = content.Key
acceptUserID = mach.Client.UserID
acceptDeviceID = mach.Client.DeviceID
acceptKey = string(verState.sas.GetPubkey())
}
// use the prefered SAS method to generate a SAS
sasMethod := verState.chosenSASMethod
sas, err := sasMethod.GetVerificationSAS(initUserID, initDeviceID, initKey, acceptUserID, acceptDeviceID, acceptKey, transactionID, verState.sas)
if err != nil {
mach.Log.Error().Msgf("Error generating SAS (method %v): %v", sasMethod.Type(), err)
return
}
mach.Log.Debug().Msgf("Generated SAS (%v): %v", sasMethod.Type(), sas)
go func() {
result := verState.hooks.VerifySASMatch(device, sas)
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(ctx context.Context, didMatch bool, transactionID string, verState *verificationState) {
verState.lock.Lock()
defer verState.lock.Unlock()
verState.extendTimeout()
if didMatch {
verState.sasMatched <- true
var err error
if verState.inRoomID == "" {
err = mach.SendSASVerificationMAC(ctx, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas)
} else {
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)
}
} else {
verState.sasMatched <- false
}
}
// 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(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(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("Error getting transaction state: %v", err)
return
}
verState.lock.Lock()
defer verState.lock.Unlock()
verState.extendTimeout()
device := verState.otherDevice
// we are done with this SAS verification in all cases so we forget about it
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
if !verState.verificationStarted || !verState.keyReceived {
// unexpected MAC at this point
mach.Log.Warn().Msgf("Unexpected MAC message for transaction %v", transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Unexpected MAC message", event.VerificationCancelUnexpectedMessage)
return
}
// do this in another goroutine as the match result might take a long time to arrive
go func() {
matched := <-verState.sasMatched
verState.lock.Lock()
defer verState.lock.Unlock()
if !matched {
mach.Log.Warn().Msgf("SAS do not match! Canceling transaction %v", transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "SAS do not match", event.VerificationCancelSASMismatch)
return
}
keyID := id.NewKeyID(id.KeyAlgorithmEd25519, device.DeviceID.String())
expectedPKMAC, expectedKeysMAC, err := mach.getPKAndKeysMAC(verState.sas, device.UserID, device.DeviceID,
mach.Client.UserID, mach.Client.DeviceID, transactionID, device.SigningKey, keyID, content.Mac)
if err != nil {
mach.Log.Error().Msgf("Error generating MAC to match with received MAC: %v", err)
return
}
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(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(ctx, verState, transactionID, "Mismatched PK MACs", event.VerificationCancelKeyMismatch)
return
}
// we can finally trust this device
device.Trust = id.TrustStateVerified
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(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(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(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)
}
}
}
} else {
// TODO ask user to unlock cross-signing keys?
mach.Log.Debug().Msgf("Cross-signing keys not cached, not signing %s/%s", device.UserID, device.DeviceID)
}
mach.Log.Debug().Msgf("Device %v of user %v verified successfully!", device.DeviceID, device.UserID)
verState.hooks.OnSuccess()
}()
}
// handleVerificationCancel handles an incoming m.key.verification.cancel message.
// It cancels the verification process for the given reason.
func (mach *OlmMachine) handleVerificationCancel(userID id.UserID, content *event.VerificationCancelEventContent, transactionID string) {
// make sure to not reply with a cancel to not cause a loop of cancel messages
// this verification will get canceled even if the senders do not match
verStateInterface, ok := mach.keyVerificationTransactionState.Load(userID.String() + ":" + transactionID)
if ok {
go verStateInterface.(*verificationState).hooks.OnCancel(false, content.Reason, content.Code)
}
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
mach.Log.Warn().Msgf("SAS verification %v was canceled by %v with reason: %v (%v)",
transactionID, userID, content.Reason, content.Code)
}
// handleVerificationRequest handles an incoming m.key.verification.request message.
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(ctx, userID, content.FromDevice)
if err != nil {
mach.Log.Error().Msgf("Could not find device %v of user %v", content.FromDevice, userID)
return
}
if !content.SupportsVerificationMethod(event.VerificationMethodSAS) {
mach.Log.Warn().Msgf("Canceling verification transaction %v as SAS is not supported", transactionID)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod)
}
return
}
resp, hooks := mach.AcceptVerificationFrom(transactionID, otherDevice, inRoomID)
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(ctx, otherDevice, hooks, transactionID, mach.DefaultSASTimeout)
} else {
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(ctx, inRoomID, otherDevice, hooks, transactionID, mach.DefaultSASTimeout)
}
}
if err != nil {
mach.Log.Error().Msgf("Error accepting SAS verification request: %v", err)
}
} 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(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
} else {
_ = 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)
}
}
// 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(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(ctx context.Context, device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) {
if transactionID == "" {
transactionID = strconv.Itoa(rand.Int())
}
mach.Log.Debug().Msgf("Starting new verification transaction %v with device %v of user %v", transactionID, device.DeviceID, device.UserID)
verState := &verificationState{
sas: olm.NewSAS(),
otherDevice: device,
initiatedByUs: true,
verificationStarted: false,
keyReceived: false,
sasMatched: make(chan bool, 1),
hooks: hooks,
}
verState.lock.Lock()
defer verState.lock.Unlock()
startEvent, err := mach.SendSASVerificationStart(ctx, device.UserID, device.DeviceID, transactionID, hooks.VerificationMethods())
if err != nil {
return "", err
}
payload, err := json.Marshal(startEvent)
if err != nil {
return "", err
}
canonical, err := canonicaljson.CanonicalJSON(payload)
if err != nil {
return "", err
}
verState.startEventCanonical = string(canonical)
_, loaded := mach.keyVerificationTransactionState.LoadOrStore(device.UserID.String()+":"+transactionID, verState)
if loaded {
return "", ErrTransactionAlreadyExists
}
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(ctx context.Context, userID id.UserID, transactionID, reason string) error {
mapKey := userID.String() + ":" + transactionID
verStateInterface, ok := mach.keyVerificationTransactionState.Load(mapKey)
if !ok {
return ErrUnknownTransaction
}
verState := verStateInterface.(*verificationState)
verState.lock.Lock()
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(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(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(ctx, userID, deviceID, event.ToDeviceVerificationCancel, content)
}
// SendSASVerificationStart is used to manually send the SAS verification start message to another device.
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()
}
content := &event.VerificationStartEventContent{
FromDevice: mach.Client.DeviceID,
TransactionID: transactionID,
Method: event.VerificationMethodSAS,
KeyAgreementProtocols: []event.KeyAgreementProtocol{event.KeyAgreementCurve25519HKDFSHA256},
Hashes: []event.VerificationHashMethod{event.VerificationHashSHA256},
MessageAuthenticationCodes: []event.MACMethod{event.HKDFHMACSHA256},
ShortAuthenticationString: sasMethods,
}
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(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(ctx, fromUser, startEvent.FromDevice, startEvent.TransactionID, reason, event.VerificationCancelUnknownMethod); err != nil {
return err
}
return ErrUnknownVerificationMethod
}
payload, err := json.Marshal(startEvent)
if err != nil {
return err
}
canonical, err := canonicaljson.CanonicalJSON(payload)
if err != nil {
return err
}
hash := olm.NewUtility().Sha256(string(publicKey) + string(canonical))
sasMethods := make([]event.SASMethod, len(methods))
for i, method := range methods {
sasMethods[i] = method.Type()
}
content := &event.VerificationAcceptEventContent{
TransactionID: startEvent.TransactionID,
Method: event.VerificationMethodSAS,
KeyAgreementProtocol: event.KeyAgreementCurve25519HKDFSHA256,
Hash: event.VerificationHashSHA256,
MessageAuthenticationCode: event.HKDFHMACSHA256,
ShortAuthenticationString: sasMethods,
Commitment: hash,
}
return mach.sendToOneDevice(ctx, fromUser, startEvent.FromDevice, event.ToDeviceVerificationAccept, content)
}
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(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(ctx context.Context, userID id.UserID, deviceID id.DeviceID, transactionID string, key string) error {
content := &event.VerificationKeyEventContent{
TransactionID: transactionID,
Key: key,
}
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(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()
keyIDsMap := map[id.KeyID]string{keyID: ""}
macMap := make(map[id.KeyID]string)
if mach.CrossSigningKeys != nil {
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.String())
// add master key ID to key map
keyIDsMap[masterKeyID] = ""
masterKeyMAC, _, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID,
userID, deviceID, transactionID, masterKey, masterKeyID, keyIDsMap)
if err != nil {
mach.Log.Error().Msgf("Error generating master key MAC: %v", err)
} else {
mach.Log.Debug().Msgf("Generated master key `%v` MAC: %v", masterKey, masterKeyMAC)
macMap[masterKeyID] = masterKeyMAC
}
}
pubKeyMac, keysMac, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID, userID, deviceID, transactionID, signingKey, keyID, keyIDsMap)
if err != nil {
return err
}
mach.Log.Debug().Msgf("MAC of key %s is: %s", signingKey, pubKeyMac)
mach.Log.Debug().Msgf("MAC of key ID(s) %s is: %s", keyID, keysMac)
macMap[keyID] = pubKeyMac
content := &event.VerificationMacEventContent{
TransactionID: transactionID,
Keys: keysMac,
Mac: macMap,
}
return mach.sendToOneDevice(ctx, userID, deviceID, event.ToDeviceVerificationMAC, content)
}
func commonSASMethods(hooks VerificationHooks, otherDeviceMethods []event.SASMethod) []VerificationMethod {
methods := make([]VerificationMethod, 0)
for _, hookMethod := range hooks.VerificationMethods() {
for _, otherMethod := range otherDeviceMethods {
if hookMethod.Type() == otherMethod {
methods = append(methods, hookMethod)
break
}
}
}
return methods
}

View File

@@ -1,334 +0,0 @@
// Copyright (c) 2020 Nikos Filippakis
//
// 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 crypto
import (
"context"
"encoding/json"
"errors"
"time"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var (
ErrNoVerificationFromDevice = errors.New("from_device field is empty")
ErrNoVerificationMethods = errors.New("verification method list is empty")
ErrNoRelatesTo = errors.New("missing m.relates_to info")
)
// ProcessInRoomVerification is a callback that is to be called when a client receives a message
// related to in-room verification.
//
// Currently this is not automatically called, so you must add the listener yourself.
// Note that in-room verification events are wrapped in m.room.encrypted, but this expects the decrypted events.
func (mach *OlmMachine) ProcessInRoomVerification(evt *event.Event) error {
if evt.Sender == mach.Client.UserID {
// nothing to do if the message is our own
return nil
}
if relatable, ok := evt.Content.Parsed.(event.Relatable); !ok || relatable.OptionalGetRelatesTo() == nil {
return ErrNoRelatesTo
}
ctx := context.TODO()
switch content := evt.Content.Parsed.(type) {
case *event.MessageEventContent:
if content.MsgType == event.MsgVerificationRequest {
if content.FromDevice == "" {
return ErrNoVerificationFromDevice
}
if content.Methods == nil {
return ErrNoVerificationMethods
}
newContent := &event.VerificationRequestEventContent{
FromDevice: content.FromDevice,
Methods: content.Methods,
Timestamp: evt.Timestamp,
TransactionID: evt.ID.String(),
}
mach.handleVerificationRequest(ctx, evt.Sender, newContent, evt.ID.String(), evt.RoomID)
}
case *event.VerificationStartEventContent:
mach.handleVerificationStart(ctx, evt.Sender, content, content.RelatesTo.EventID.String(), 10*time.Minute, evt.RoomID)
case *event.VerificationReadyEventContent:
mach.handleInRoomVerificationReady(ctx, evt.Sender, evt.RoomID, content, content.RelatesTo.EventID.String())
case *event.VerificationAcceptEventContent:
mach.handleVerificationAccept(ctx, evt.Sender, content, content.RelatesTo.EventID.String())
case *event.VerificationKeyEventContent:
mach.handleVerificationKey(ctx, evt.Sender, content, content.RelatesTo.EventID.String())
case *event.VerificationMacEventContent:
mach.handleVerificationMAC(ctx, evt.Sender, content, content.RelatesTo.EventID.String())
case *event.VerificationCancelEventContent:
mach.handleVerificationCancel(evt.Sender, content, content.RelatesTo.EventID.String())
}
return nil
}
// SendInRoomSASVerificationCancel is used to manually send an in-room SAS cancel message process with the given reason and cancellation code.
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,
Code: code,
To: userID,
}
encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationCancel, content)
if err != nil {
return err
}
_, 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(ctx context.Context, roomID id.RoomID, toUserID id.UserID, methods []VerificationMethod) (string, error) {
content := &event.MessageEventContent{
MsgType: event.MsgVerificationRequest,
FromDevice: mach.Client.DeviceID,
Methods: []event.VerificationMethod{event.VerificationMethodSAS},
To: toUserID,
}
encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.EventMessage, content)
if err != nil {
return "", err
}
resp, err := mach.Client.SendMessageEvent(ctx, roomID, event.EventEncrypted, encrypted)
if err != nil {
return "", err
}
return resp.EventID.String(), nil
}
// SendInRoomSASVerificationReady is used to manually send an in-room SAS verification ready message to another user.
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(ctx, roomID, event.InRoomVerificationReady, content)
if err != nil {
return err
}
_, 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(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()
}
content := &event.VerificationStartEventContent{
FromDevice: mach.Client.DeviceID,
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
Method: event.VerificationMethodSAS,
KeyAgreementProtocols: []event.KeyAgreementProtocol{event.KeyAgreementCurve25519HKDFSHA256},
Hashes: []event.VerificationHashMethod{event.VerificationHashSHA256},
MessageAuthenticationCodes: []event.MACMethod{event.HKDFHMACSHA256},
ShortAuthenticationString: sasMethods,
To: toUserID,
}
encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationStart, content)
if err != nil {
return nil, err
}
_, 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(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(ctx, roomID, fromUser, transactionID, reason, event.VerificationCancelUnknownMethod); err != nil {
return err
}
return ErrUnknownVerificationMethod
}
payload, err := json.Marshal(startEvent)
if err != nil {
return err
}
canonical, err := canonicaljson.CanonicalJSON(payload)
if err != nil {
return err
}
hash := olm.NewUtility().Sha256(string(publicKey) + string(canonical))
sasMethods := make([]event.SASMethod, len(methods))
for i, method := range methods {
sasMethods[i] = method.Type()
}
content := &event.VerificationAcceptEventContent{
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
Method: event.VerificationMethodSAS,
KeyAgreementProtocol: event.KeyAgreementCurve25519HKDFSHA256,
Hash: event.VerificationHashSHA256,
MessageAuthenticationCode: event.HKDFHMACSHA256,
ShortAuthenticationString: sasMethods,
Commitment: hash,
To: fromUser,
}
encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationAccept, content)
if err != nil {
return err
}
_, 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(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(ctx, roomID, event.InRoomVerificationKey, content)
if err != nil {
return err
}
_, 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(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()
keyIDsMap := map[id.KeyID]string{keyID: ""}
macMap := make(map[id.KeyID]string)
if mach.CrossSigningKeys != nil {
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.String())
// add master key ID to key map
keyIDsMap[masterKeyID] = ""
masterKeyMAC, _, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID,
userID, deviceID, transactionID, masterKey, masterKeyID, keyIDsMap)
if err != nil {
mach.Log.Error().Msgf("Error generating master key MAC: %v", err)
} else {
mach.Log.Debug().Msgf("Generated master key `%v` MAC: %v", masterKey, masterKeyMAC)
macMap[masterKeyID] = masterKeyMAC
}
}
pubKeyMac, keysMac, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID, userID, deviceID, transactionID, signingKey, keyID, keyIDsMap)
if err != nil {
return err
}
mach.Log.Debug().Msgf("MAC of key %s is: %s", signingKey, pubKeyMac)
mach.Log.Debug().Msgf("MAC of key ID(s) %s is: %s", keyID, keysMac)
macMap[keyID] = pubKeyMac
content := &event.VerificationMacEventContent{
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
Keys: keysMac,
Mac: macMap,
To: userID,
}
encrypted, err := mach.EncryptMegolmEvent(ctx, roomID, event.InRoomVerificationMAC, content)
if err != nil {
return err
}
_, 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(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(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(ctx, inRoomID, device.UserID, hooks.VerificationMethods())
if err != nil {
return "", err
}
}
verState := &verificationState{
sas: olm.NewSAS(),
otherDevice: device,
initiatedByUs: true,
verificationStarted: false,
keyReceived: false,
sasMatched: make(chan bool, 1),
hooks: hooks,
inRoomID: inRoomID,
}
verState.lock.Lock()
defer verState.lock.Unlock()
if !request {
// start in-room verification
startEvent, err := mach.SendInRoomSASVerificationStart(ctx, inRoomID, device.UserID, transactionID, hooks.VerificationMethods())
if err != nil {
return "", err
}
payload, err := json.Marshal(startEvent)
if err != nil {
return "", err
}
canonical, err := canonicaljson.CanonicalJSON(payload)
if err != nil {
return "", err
}
verState.startEventCanonical = string(canonical)
}
mach.keyVerificationTransactionState.Store(device.UserID.String()+":"+transactionID, verState)
mach.timeoutAfter(ctx, verState, transactionID, timeout)
return transactionID, nil
}
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(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("Error getting transaction state: %v", err)
return
}
//mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
if mach.Client.UserID < userID {
// up to us to send the start message
verState.lock.Lock()
mach.newInRoomSASVerificationWithInner(ctx, roomID, device, verState.hooks, transactionID, 10*time.Minute)
verState.lock.Unlock()
}
}

View File

@@ -1,201 +0,0 @@
// Copyright (c) 2020 Nikos Filippakis
//
// 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 crypto
import (
"fmt"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// SASData contains the data that users need to verify.
type SASData interface {
Type() event.SASMethod
}
// VerificationMethod describes a method for generating a SAS.
type VerificationMethod interface {
// GetVerificationSAS uses the user, device ID and key of the user who initiated the verification transaction,
// the user, device ID and key of the user who accepted, the transaction ID and the SAS object to generate a SAS.
// The SAS can be any type, such as an array of numbers or emojis.
GetVerificationSAS(initUserID id.UserID, initDeviceID id.DeviceID, initKey string,
acceptUserID id.UserID, acceptDeviceID id.DeviceID, acceptKey string,
transactionID string, sas *olm.SAS) (SASData, error)
// Type returns the type of this SAS method
Type() event.SASMethod
}
const sasInfoFormat = "MATRIX_KEY_VERIFICATION_SAS|%s|%s|%s|%s|%s|%s|%s"
// VerificationMethodDecimal describes the decimal SAS method.
type VerificationMethodDecimal struct{}
// DecimalSASData contains the verification numbers for the decimal SAS method.
type DecimalSASData [3]uint
// Type returns the decimal SAS method type.
func (DecimalSASData) Type() event.SASMethod {
return event.SASDecimal
}
// GetVerificationSAS generates the three numbers that need to match with the other device for a verification to be valid.
func (VerificationMethodDecimal) GetVerificationSAS(initUserID id.UserID, initDeviceID id.DeviceID, initKey string,
acceptUserID id.UserID, acceptDeviceID id.DeviceID, acceptKey string,
transactionID string, sas *olm.SAS) (SASData, error) {
sasInfo := fmt.Sprintf(sasInfoFormat,
initUserID, initDeviceID, initKey,
acceptUserID, acceptDeviceID, acceptKey,
transactionID)
sasBytes, err := sas.GenerateBytes([]byte(sasInfo), 5)
if err != nil {
return DecimalSASData{0, 0, 0}, err
}
numbers := DecimalSASData{
(uint(sasBytes[0])<<5 | uint(sasBytes[1])>>3) + 1000,
(uint(sasBytes[1]&0x7)<<10 | uint(sasBytes[2])<<2 | uint(sasBytes[3]>>6)) + 1000,
(uint(sasBytes[3]&0x3F)<<7 | uint(sasBytes[4])>>1) + 1000,
}
return numbers, nil
}
// Type returns the decimal SAS method type.
func (VerificationMethodDecimal) Type() event.SASMethod {
return event.SASDecimal
}
var allEmojis = [...]VerificationEmoji{
{'🐶', "Dog"},
{'🐱', "Cat"},
{'🦁', "Lion"},
{'🐎', "Horse"},
{'🦄', "Unicorn"},
{'🐷', "Pig"},
{'🐘', "Elephant"},
{'🐰', "Rabbit"},
{'🐼', "Panda"},
{'🐓', "Rooster"},
{'🐧', "Penguin"},
{'🐢', "Turtle"},
{'🐟', "Fish"},
{'🐙', "Octopus"},
{'🦋', "Butterfly"},
{'🌷', "Flower"},
{'🌳', "Tree"},
{'🌵', "Cactus"},
{'🍄', "Mushroom"},
{'🌏', "Globe"},
{'🌙', "Moon"},
{'☁', "Cloud"},
{'🔥', "Fire"},
{'🍌', "Banana"},
{'🍎', "Apple"},
{'🍓', "Strawberry"},
{'🌽', "Corn"},
{'🍕', "Pizza"},
{'🎂', "Cake"},
{'❤', "Heart"},
{'😀', "Smiley"},
{'🤖', "Robot"},
{'🎩', "Hat"},
{'👓', "Glasses"},
{'🔧', "Spanner"},
{'🎅', "Santa"},
{'👍', "Thumbs Up"},
{'☂', "Umbrella"},
{'⌛', "Hourglass"},
{'⏰', "Clock"},
{'🎁', "Gift"},
{'💡', "Light Bulb"},
{'📕', "Book"},
{'✏', "Pencil"},
{'📎', "Paperclip"},
{'✂', "Scissors"},
{'🔒', "Lock"},
{'🔑', "Key"},
{'🔨', "Hammer"},
{'☎', "Telephone"},
{'🏁', "Flag"},
{'🚂', "Train"},
{'🚲', "Bicycle"},
{'✈', "Aeroplane"},
{'🚀', "Rocket"},
{'🏆', "Trophy"},
{'⚽', "Ball"},
{'🎸', "Guitar"},
{'🎺', "Trumpet"},
{'🔔', "Bell"},
{'⚓', "Anchor"},
{'🎧', "Headphones"},
{'📁', "Folder"},
{'📌', "Pin"},
}
// VerificationEmoji describes an emoji that might be sent for verifying devices.
type VerificationEmoji struct {
Emoji rune
Description string
}
func (vm VerificationEmoji) GetEmoji() rune {
return vm.Emoji
}
func (vm VerificationEmoji) GetDescription() string {
return vm.Description
}
// EmojiSASData contains the verification emojis for the emoji SAS method.
type EmojiSASData [7]VerificationEmoji
// Type returns the emoji SAS method type.
func (EmojiSASData) Type() event.SASMethod {
return event.SASEmoji
}
// VerificationMethodEmoji describes the emoji SAS method.
type VerificationMethodEmoji struct{}
// GetVerificationSAS generates the three numbers that need to match with the other device for a verification to be valid.
func (VerificationMethodEmoji) GetVerificationSAS(initUserID id.UserID, initDeviceID id.DeviceID, initKey string,
acceptUserID id.UserID, acceptDeviceID id.DeviceID, acceptKey string,
transactionID string, sas *olm.SAS) (SASData, error) {
sasInfo := fmt.Sprintf(sasInfoFormat,
initUserID, initDeviceID, initKey,
acceptUserID, acceptDeviceID, acceptKey,
transactionID)
var emojis EmojiSASData
sasBytes, err := sas.GenerateBytes([]byte(sasInfo), 6)
if err != nil {
return emojis, err
}
sasNum := uint64(sasBytes[0])<<40 | uint64(sasBytes[1])<<32 | uint64(sasBytes[2])<<24 |
uint64(sasBytes[3])<<16 | uint64(sasBytes[4])<<8 | uint64(sasBytes[5])
for i := 0; i < len(emojis); i++ {
// take nth group of 6 bits
emojiIdx := (sasNum >> uint(48-(i+1)*6)) & 0x3F
emoji := allEmojis[emojiIdx]
emojis[i] = emoji
}
return emojis, nil
}
// Type returns the emoji SAS method type.
func (VerificationMethodEmoji) Type() event.SASMethod {
return event.SASEmoji
}

View File

@@ -61,3 +61,27 @@ type BeeperRoomKeyAckEventContent struct {
SessionID id.SessionID `json:"session_id"`
FirstMessageIndex int `json:"first_message_index"`
}
type LinkPreview struct {
CanonicalURL string `json:"og:url,omitempty"`
Title string `json:"og:title,omitempty"`
Type string `json:"og:type,omitempty"`
Description string `json:"og:description,omitempty"`
ImageURL id.ContentURIString `json:"og:image,omitempty"`
ImageSize int `json:"matrix:image:size,omitempty"`
ImageWidth int `json:"og:image:width,omitempty"`
ImageHeight int `json:"og:image:height,omitempty"`
ImageType string `json:"og:image:type,omitempty"`
}
// BeeperLinkPreview contains the data for a bundled URL preview as specified in MSC4095
//
// https://github.com/matrix-org/matrix-spec-proposals/pull/4095
type BeeperLinkPreview struct {
LinkPreview
MatchedURL string `json:"matched_url,omitempty"`
ImageEncryption *EncryptedFileInfo `json:"beeper:image:encryption,omitempty"`
}

View File

@@ -57,26 +57,33 @@ var TypeMap = map[Type]reflect.Type{
EphemeralEventReceipt: reflect.TypeOf(ReceiptEventContent{}),
EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}),
InRoomVerificationStart: reflect.TypeOf(VerificationStartEventContent{}),
InRoomVerificationReady: reflect.TypeOf(VerificationReadyEventContent{}),
InRoomVerificationStart: reflect.TypeOf(VerificationStartEventContent{}),
InRoomVerificationDone: reflect.TypeOf(VerificationDoneEventContent{}),
InRoomVerificationCancel: reflect.TypeOf(VerificationCancelEventContent{}),
InRoomVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}),
InRoomVerificationKey: reflect.TypeOf(VerificationKeyEventContent{}),
InRoomVerificationMAC: reflect.TypeOf(VerificationMacEventContent{}),
InRoomVerificationCancel: reflect.TypeOf(VerificationCancelEventContent{}),
InRoomVerificationMAC: reflect.TypeOf(VerificationMACEventContent{}),
ToDeviceRoomKey: reflect.TypeOf(RoomKeyEventContent{}),
ToDeviceForwardedRoomKey: reflect.TypeOf(ForwardedRoomKeyEventContent{}),
ToDeviceRoomKeyRequest: reflect.TypeOf(RoomKeyRequestEventContent{}),
ToDeviceEncrypted: reflect.TypeOf(EncryptedEventContent{}),
ToDeviceRoomKeyWithheld: reflect.TypeOf(RoomKeyWithheldEventContent{}),
ToDeviceSecretRequest: reflect.TypeOf(SecretRequestEventContent{}),
ToDeviceSecretSend: reflect.TypeOf(SecretSendEventContent{}),
ToDeviceDummy: reflect.TypeOf(DummyEventContent{}),
ToDeviceVerificationStart: reflect.TypeOf(VerificationStartEventContent{}),
ToDeviceVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}),
ToDeviceVerificationKey: reflect.TypeOf(VerificationKeyEventContent{}),
ToDeviceVerificationMAC: reflect.TypeOf(VerificationMacEventContent{}),
ToDeviceVerificationCancel: reflect.TypeOf(VerificationCancelEventContent{}),
ToDeviceVerificationRequest: reflect.TypeOf(VerificationRequestEventContent{}),
ToDeviceVerificationReady: reflect.TypeOf(VerificationReadyEventContent{}),
ToDeviceVerificationStart: reflect.TypeOf(VerificationStartEventContent{}),
ToDeviceVerificationDone: reflect.TypeOf(VerificationDoneEventContent{}),
ToDeviceVerificationCancel: reflect.TypeOf(VerificationCancelEventContent{}),
ToDeviceVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}),
ToDeviceVerificationKey: reflect.TypeOf(VerificationKeyEventContent{}),
ToDeviceVerificationMAC: reflect.TypeOf(VerificationMACEventContent{}),
ToDeviceOrgMatrixRoomKeyWithheld: reflect.TypeOf(RoomKeyWithheldEventContent{}),
@@ -506,3 +513,59 @@ func (content *Content) AsModPolicy() *ModPolicyContent {
}
return casted
}
func (content *Content) AsVerificationRequest() *VerificationRequestEventContent {
casted, ok := content.Parsed.(*VerificationRequestEventContent)
if !ok {
return &VerificationRequestEventContent{}
}
return casted
}
func (content *Content) AsVerificationReady() *VerificationReadyEventContent {
casted, ok := content.Parsed.(*VerificationReadyEventContent)
if !ok {
return &VerificationReadyEventContent{}
}
return casted
}
func (content *Content) AsVerificationStart() *VerificationStartEventContent {
casted, ok := content.Parsed.(*VerificationStartEventContent)
if !ok {
return &VerificationStartEventContent{}
}
return casted
}
func (content *Content) AsVerificationDone() *VerificationDoneEventContent {
casted, ok := content.Parsed.(*VerificationDoneEventContent)
if !ok {
return &VerificationDoneEventContent{}
}
return casted
}
func (content *Content) AsVerificationCancel() *VerificationCancelEventContent {
casted, ok := content.Parsed.(*VerificationCancelEventContent)
if !ok {
return &VerificationCancelEventContent{}
}
return casted
}
func (content *Content) AsVerificationAccept() *VerificationAcceptEventContent {
casted, ok := content.Parsed.(*VerificationAcceptEventContent)
if !ok {
return &VerificationAcceptEventContent{}
}
return casted
}
func (content *Content) AsVerificationKey() *VerificationKeyEventContent {
casted, ok := content.Parsed.(*VerificationKeyEventContent)
if !ok {
return &VerificationKeyEventContent{}
}
return casted
}
func (content *Content) AsVerificationMAC() *VerificationMACEventContent {
casted, ok := content.Parsed.(*VerificationMACEventContent)
if !ok {
return &VerificationMACEventContent{}
}
return casted
}

View File

@@ -176,4 +176,27 @@ func (withheld *RoomKeyWithheldEventContent) Is(other error) bool {
return withheld.Code == "" || otherWithheld.Code == "" || withheld.Code == otherWithheld.Code
}
type SecretRequestAction string
func (a SecretRequestAction) String() string {
return string(a)
}
const (
SecretRequestRequest = "request"
SecretRequestCancellation = "request_cancellation"
)
type SecretRequestEventContent struct {
Name id.Secret `json:"name,omitempty"`
Action SecretRequestAction `json:"action"`
RequestingDeviceID id.DeviceID `json:"requesting_device_id"`
RequestID string `json:"request_id"`
}
type SecretSendEventContent struct {
RequestID string `json:"request_id"`
Secret string `json:"secret"`
}
type DummyEventContent struct{}

View File

@@ -116,6 +116,8 @@ type MessageEventContent struct {
BeeperGalleryImages []*MessageEventContent `json:"com.beeper.gallery.images,omitempty"`
BeeperGalleryCaption string `json:"com.beeper.gallery.caption,omitempty"`
BeeperGalleryCaptionHTML string `json:"com.beeper.gallery.caption_html,omitempty"`
BeeperLinkPreviews []*BeeperLinkPreview `json:"com.beeper.linkpreviews,omitempty"`
}
func (content *MessageEventContent) GetRelatesTo() *RelatesTo {

View File

@@ -10,6 +10,8 @@ import (
"encoding/json"
"fmt"
"strings"
"maunium.net/go/mautrix/id"
)
type RoomType string
@@ -116,7 +118,8 @@ func (et *Type) GuessClass() TypeClass {
return EphemeralEventType
case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type,
AccountDataSecretStorageKey.Type, AccountDataSecretStorageDefaultKey.Type,
AccountDataCrossSigningMaster.Type, AccountDataCrossSigningSelf.Type, AccountDataCrossSigningUser.Type:
AccountDataCrossSigningMaster.Type, AccountDataCrossSigningSelf.Type, AccountDataCrossSigningUser.Type,
AccountDataFullyRead.Type, AccountDataMegolmBackupKey.Type:
return AccountDataEventType
case EventRedaction.Type, EventMessage.Type, EventEncrypted.Type, EventReaction.Type, EventSticker.Type,
InRoomVerificationStart.Type, InRoomVerificationReady.Type, InRoomVerificationAccept.Type,
@@ -200,12 +203,15 @@ var (
EventReaction = Type{"m.reaction", MessageEventType}
EventSticker = Type{"m.sticker", MessageEventType}
InRoomVerificationStart = Type{"m.key.verification.start", MessageEventType}
InRoomVerificationReady = Type{"m.key.verification.ready", MessageEventType}
InRoomVerificationStart = Type{"m.key.verification.start", MessageEventType}
InRoomVerificationDone = Type{"m.key.verification.done", MessageEventType}
InRoomVerificationCancel = Type{"m.key.verification.cancel", MessageEventType}
// SAS Verification Events
InRoomVerificationAccept = Type{"m.key.verification.accept", MessageEventType}
InRoomVerificationKey = Type{"m.key.verification.key", MessageEventType}
InRoomVerificationMAC = Type{"m.key.verification.mac", MessageEventType}
InRoomVerificationCancel = Type{"m.key.verification.cancel", MessageEventType}
CallInvite = Type{"m.call.invite", MessageEventType}
CallCandidates = Type{"m.call.candidates", MessageEventType}
@@ -235,26 +241,34 @@ var (
AccountDataSecretStorageDefaultKey = Type{"m.secret_storage.default_key", AccountDataEventType}
AccountDataSecretStorageKey = Type{"m.secret_storage.key", AccountDataEventType}
AccountDataCrossSigningMaster = Type{"m.cross_signing.master", AccountDataEventType}
AccountDataCrossSigningUser = Type{"m.cross_signing.user_signing", AccountDataEventType}
AccountDataCrossSigningSelf = Type{"m.cross_signing.self_signing", AccountDataEventType}
AccountDataCrossSigningMaster = Type{string(id.SecretXSMaster), AccountDataEventType}
AccountDataCrossSigningUser = Type{string(id.SecretXSUserSigning), AccountDataEventType}
AccountDataCrossSigningSelf = Type{string(id.SecretXSSelfSigning), AccountDataEventType}
AccountDataMegolmBackupKey = Type{"m.megolm_backup.v1", AccountDataEventType}
)
// Device-to-device events
var (
ToDeviceRoomKey = Type{"m.room_key", ToDeviceEventType}
ToDeviceRoomKeyRequest = Type{"m.room_key_request", ToDeviceEventType}
ToDeviceForwardedRoomKey = Type{"m.forwarded_room_key", ToDeviceEventType}
ToDeviceEncrypted = Type{"m.room.encrypted", ToDeviceEventType}
ToDeviceRoomKeyWithheld = Type{"m.room_key.withheld", ToDeviceEventType}
ToDeviceDummy = Type{"m.dummy", ToDeviceEventType}
ToDeviceRoomKey = Type{"m.room_key", ToDeviceEventType}
ToDeviceRoomKeyRequest = Type{"m.room_key_request", ToDeviceEventType}
ToDeviceForwardedRoomKey = Type{"m.forwarded_room_key", ToDeviceEventType}
ToDeviceEncrypted = Type{"m.room.encrypted", ToDeviceEventType}
ToDeviceRoomKeyWithheld = Type{"m.room_key.withheld", ToDeviceEventType}
ToDeviceSecretRequest = Type{"m.secret.request", ToDeviceEventType}
ToDeviceSecretSend = Type{"m.secret.send", ToDeviceEventType}
ToDeviceDummy = Type{"m.dummy", ToDeviceEventType}
ToDeviceVerificationRequest = Type{"m.key.verification.request", ToDeviceEventType}
ToDeviceVerificationReady = Type{"m.key.verification.ready", ToDeviceEventType}
ToDeviceVerificationStart = Type{"m.key.verification.start", ToDeviceEventType}
ToDeviceVerificationAccept = Type{"m.key.verification.accept", ToDeviceEventType}
ToDeviceVerificationKey = Type{"m.key.verification.key", ToDeviceEventType}
ToDeviceVerificationMAC = Type{"m.key.verification.mac", ToDeviceEventType}
ToDeviceVerificationDone = Type{"m.key.verification.done", ToDeviceEventType}
ToDeviceVerificationCancel = Type{"m.key.verification.cancel", ToDeviceEventType}
// SAS Verification Events
ToDeviceVerificationAccept = Type{"m.key.verification.accept", ToDeviceEventType}
ToDeviceVerificationKey = Type{"m.key.verification.key", ToDeviceEventType}
ToDeviceVerificationMAC = Type{"m.key.verification.mac", ToDeviceEventType}
ToDeviceOrgMatrixRoomKeyWithheld = Type{"org.matrix.room_key.withheld", ToDeviceEventType}
ToDeviceBeeperRoomKeyAck = Type{"com.beeper.room_key.ack", ToDeviceEventType}

View File

@@ -7,301 +7,298 @@
package event
import (
"go.mau.fi/util/jsonbytes"
"go.mau.fi/util/jsontime"
"maunium.net/go/mautrix/id"
)
type VerificationMethod string
const VerificationMethodSAS VerificationMethod = "m.sas.v1"
const (
VerificationMethodSAS VerificationMethod = "m.sas.v1"
// VerificationRequestEventContent represents the content of a m.key.verification.request to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationrequest
type VerificationRequestEventContent struct {
// The device ID which is initiating the request.
FromDevice id.DeviceID `json:"from_device"`
// An opaque identifier for the verification request. Must be unique with respect to the devices involved.
TransactionID string `json:"transaction_id,omitempty"`
// The verification methods supported by the sender.
Methods []VerificationMethod `json:"methods"`
// The POSIX timestamp in milliseconds for when the request was made.
Timestamp int64 `json:"timestamp,omitempty"`
// The user that the event is sent to for in-room verification.
To id.UserID `json:"to,omitempty"`
// Original event ID for in-room verification.
VerificationMethodReciprocate VerificationMethod = "m.reciprocate.v1"
VerificationMethodQRCodeShow VerificationMethod = "m.qr_code.show.v1"
VerificationMethodQRCodeScan VerificationMethod = "m.qr_code.scan.v1"
)
type VerificationTransactionable interface {
GetTransactionID() id.VerificationTransactionID
SetTransactionID(id.VerificationTransactionID)
}
// ToDeviceVerificationEvent contains the fields common to all to-device
// verification events.
type ToDeviceVerificationEvent struct {
// TransactionID is an opaque identifier for the verification request. Must
// be unique with respect to the devices involved.
TransactionID id.VerificationTransactionID `json:"transaction_id,omitempty"`
}
var _ VerificationTransactionable = (*ToDeviceVerificationEvent)(nil)
func (ve *ToDeviceVerificationEvent) GetTransactionID() id.VerificationTransactionID {
return ve.TransactionID
}
func (ve *ToDeviceVerificationEvent) SetTransactionID(id id.VerificationTransactionID) {
ve.TransactionID = id
}
// InRoomVerificationEvent contains the fields common to all in-room
// verification events.
type InRoomVerificationEvent struct {
// RelatesTo indicates the m.key.verification.request that this message is
// related to. Note that for encrypted messages, this property should be in
// the unencrypted portion of the event.
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
}
func (vrec *VerificationRequestEventContent) SupportsVerificationMethod(meth VerificationMethod) bool {
for _, supportedMeth := range vrec.Methods {
if supportedMeth == meth {
return true
}
var _ Relatable = (*InRoomVerificationEvent)(nil)
func (ve *InRoomVerificationEvent) GetRelatesTo() *RelatesTo {
if ve.RelatesTo == nil {
ve.RelatesTo = &RelatesTo{}
}
return false
return ve.RelatesTo
}
func (ve *InRoomVerificationEvent) OptionalGetRelatesTo() *RelatesTo {
return ve.RelatesTo
}
func (ve *InRoomVerificationEvent) SetRelatesTo(rel *RelatesTo) {
ve.RelatesTo = rel
}
// VerificationRequestEventContent represents the content of an
// [m.key.verification.request] to-device event as described in [Section
// 11.12.2.1] of the Spec.
//
// For the in-room version, use a standard [MessageEventContent] struct.
//
// [m.key.verification.request]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationrequest
// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
type VerificationRequestEventContent struct {
ToDeviceVerificationEvent
// FromDevice is the device ID which is initiating the request.
FromDevice id.DeviceID `json:"from_device"`
// Methods is a list of the verification methods supported by the sender.
Methods []VerificationMethod `json:"methods"`
// Timestamp is the time at which the request was made.
Timestamp jsontime.UnixMilli `json:"timestamp,omitempty"`
}
// VerificationRequestEventContentFromMessage converts an in-room verification
// request message event to a [VerificationRequestEventContent].
func VerificationRequestEventContentFromMessage(evt *Event) *VerificationRequestEventContent {
content := evt.Content.AsMessage()
return &VerificationRequestEventContent{
ToDeviceVerificationEvent: ToDeviceVerificationEvent{
TransactionID: id.VerificationTransactionID(evt.ID),
},
Timestamp: jsontime.UMInt(evt.Timestamp),
FromDevice: content.FromDevice,
Methods: content.Methods,
}
}
// VerificationReadyEventContent represents the content of an
// [m.key.verification.ready] event (both the to-device and the in-room
// version) as described in [Section 11.12.2.1] of the Spec.
//
// [m.key.verification.ready]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationready
// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
type VerificationReadyEventContent struct {
ToDeviceVerificationEvent
InRoomVerificationEvent
// FromDevice is the device ID which is initiating the request.
FromDevice id.DeviceID `json:"from_device"`
// Methods is a list of the verification methods supported by the sender.
Methods []VerificationMethod `json:"methods"`
}
type KeyAgreementProtocol string
const (
KeyAgreementCurve25519 KeyAgreementProtocol = "curve25519"
KeyAgreementCurve25519HKDFSHA256 KeyAgreementProtocol = "curve25519-hkdf-sha256"
KeyAgreementProtocolCurve25519 KeyAgreementProtocol = "curve25519"
KeyAgreementProtocolCurve25519HKDFSHA256 KeyAgreementProtocol = "curve25519-hkdf-sha256"
)
type VerificationHashMethod string
const VerificationHashSHA256 VerificationHashMethod = "sha256"
const VerificationHashMethodSHA256 VerificationHashMethod = "sha256"
type MACMethod string
const HKDFHMACSHA256 MACMethod = "hkdf-hmac-sha256"
const (
MACMethodHKDFHMACSHA256 MACMethod = "hkdf-hmac-sha256"
MACMethodHKDFHMACSHA256V2 MACMethod = "hkdf-hmac-sha256.v2"
)
type SASMethod string
const (
SASDecimal SASMethod = "decimal"
SASEmoji SASMethod = "emoji"
SASMethodDecimal SASMethod = "decimal"
SASMethodEmoji SASMethod = "emoji"
)
// VerificationStartEventContent represents the content of a m.key.verification.start to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationstartmsasv1
// VerificationStartEventContent represents the content of an
// [m.key.verification.start] event (both the to-device and the in-room
// version) as described in [Section 11.12.2.1] of the Spec.
//
// This struct also contains the fields for an [m.key.verification.start] event
// using the [VerificationMethodSAS] method as described in [Section
// 11.12.2.2.2] and an [m.key.verification.start] using
// [VerificationMethodReciprocate] as described in [Section 11.12.2.4.2].
//
// [m.key.verification.start]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationstart
// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
// [Section 11.12.2.4.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-qr-codes
type VerificationStartEventContent struct {
// The device ID which is initiating the process.
ToDeviceVerificationEvent
InRoomVerificationEvent
// FromDevice is the device ID which is initiating the request.
FromDevice id.DeviceID `json:"from_device"`
// An opaque identifier for the verification process. Must be unique with respect to the devices involved.
TransactionID string `json:"transaction_id,omitempty"`
// The verification method to use.
// Method is the verification method to use.
Method VerificationMethod `json:"method"`
// The key agreement protocols the sending device understands.
KeyAgreementProtocols []KeyAgreementProtocol `json:"key_agreement_protocols"`
// The hash methods the sending device understands.
Hashes []VerificationHashMethod `json:"hashes"`
// The message authentication codes that the sending device understands.
// NextMethod is an optional method to use to verify the other user's key.
// Applicable when the method chosen only verifies one users key. This
// field will never be present if the method verifies keys both ways.
NextMethod VerificationMethod `json:"next_method,omitempty"`
// Hashes are the hash methods the sending device understands. This field
// is only applicable when the method is m.sas.v1.
Hashes []VerificationHashMethod `json:"hashes,omitempty"`
// KeyAgreementProtocols is the list of key agreement protocols the sending
// device understands. This field is only applicable when the method is
// m.sas.v1.
KeyAgreementProtocols []KeyAgreementProtocol `json:"key_agreement_protocols,omitempty"`
// MessageAuthenticationCodes is a list of the MAC methods that the sending
// device understands. This field is only applicable when the method is
// m.sas.v1.
MessageAuthenticationCodes []MACMethod `json:"message_authentication_codes"`
// The SAS methods the sending device (and the sending device's user) understands.
// ShortAuthenticationString is a list of SAS methods the sending device
// (and the sending device's user) understands. This field is only
// applicable when the method is m.sas.v1.
ShortAuthenticationString []SASMethod `json:"short_authentication_string"`
// The user that the event is sent to for in-room verification.
To id.UserID `json:"to,omitempty"`
// Original event ID for in-room verification.
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
// Secret is the shared secret from the QR code. This field is only
// applicable when the method is m.reciprocate.v1.
Secret jsonbytes.UnpaddedBytes `json:"secret,omitempty"`
}
func (vsec *VerificationStartEventContent) SupportsKeyAgreementProtocol(proto KeyAgreementProtocol) bool {
for _, supportedProto := range vsec.KeyAgreementProtocols {
if supportedProto == proto {
return true
}
}
return false
}
func (vsec *VerificationStartEventContent) SupportsHashMethod(alg VerificationHashMethod) bool {
for _, supportedAlg := range vsec.Hashes {
if supportedAlg == alg {
return true
}
}
return false
}
func (vsec *VerificationStartEventContent) SupportsMACMethod(meth MACMethod) bool {
for _, supportedMeth := range vsec.MessageAuthenticationCodes {
if supportedMeth == meth {
return true
}
}
return false
}
func (vsec *VerificationStartEventContent) SupportsSASMethod(meth SASMethod) bool {
for _, supportedMeth := range vsec.ShortAuthenticationString {
if supportedMeth == meth {
return true
}
}
return false
}
func (vsec *VerificationStartEventContent) GetRelatesTo() *RelatesTo {
if vsec.RelatesTo == nil {
vsec.RelatesTo = &RelatesTo{}
}
return vsec.RelatesTo
}
func (vsec *VerificationStartEventContent) OptionalGetRelatesTo() *RelatesTo {
return vsec.RelatesTo
}
func (vsec *VerificationStartEventContent) SetRelatesTo(rel *RelatesTo) {
vsec.RelatesTo = rel
}
// VerificationReadyEventContent represents the content of a m.key.verification.ready event.
// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationready
type VerificationReadyEventContent struct {
// The device ID which accepted the process.
FromDevice id.DeviceID `json:"from_device"`
// The verification methods supported by the sender.
Methods []VerificationMethod `json:"methods"`
// Original event ID for in-room verification.
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
}
var _ Relatable = (*VerificationReadyEventContent)(nil)
func (vrec *VerificationReadyEventContent) GetRelatesTo() *RelatesTo {
if vrec.RelatesTo == nil {
vrec.RelatesTo = &RelatesTo{}
}
return vrec.RelatesTo
}
func (vrec *VerificationReadyEventContent) OptionalGetRelatesTo() *RelatesTo {
return vrec.RelatesTo
}
func (vrec *VerificationReadyEventContent) SetRelatesTo(rel *RelatesTo) {
vrec.RelatesTo = rel
}
// VerificationAcceptEventContent represents the content of a m.key.verification.accept to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationaccept
type VerificationAcceptEventContent struct {
// An opaque identifier for the verification process. Must be the same as the one used for the m.key.verification.start message.
TransactionID string `json:"transaction_id,omitempty"`
// The verification method to use.
Method VerificationMethod `json:"method"`
// The key agreement protocol the device is choosing to use, out of the options in the m.key.verification.start message.
KeyAgreementProtocol KeyAgreementProtocol `json:"key_agreement_protocol"`
// The hash method the device is choosing to use, out of the options in the m.key.verification.start message.
Hash VerificationHashMethod `json:"hash"`
// The message authentication code the device is choosing to use, out of the options in the m.key.verification.start message.
MessageAuthenticationCode MACMethod `json:"message_authentication_code"`
// The SAS methods both devices involved in the verification process understand. Must be a subset of the options in the m.key.verification.start message.
ShortAuthenticationString []SASMethod `json:"short_authentication_string"`
// The hash (encoded as unpadded base64) of the concatenation of the device's ephemeral public key (encoded as unpadded base64) and the canonical JSON representation of the m.key.verification.start message.
Commitment string `json:"commitment"`
// The user that the event is sent to for in-room verification.
To id.UserID `json:"to,omitempty"`
// Original event ID for in-room verification.
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
}
func (vaec *VerificationAcceptEventContent) GetRelatesTo() *RelatesTo {
if vaec.RelatesTo == nil {
vaec.RelatesTo = &RelatesTo{}
}
return vaec.RelatesTo
}
func (vaec *VerificationAcceptEventContent) OptionalGetRelatesTo() *RelatesTo {
return vaec.RelatesTo
}
func (vaec *VerificationAcceptEventContent) SetRelatesTo(rel *RelatesTo) {
vaec.RelatesTo = rel
}
// VerificationKeyEventContent represents the content of a m.key.verification.key to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationkey
type VerificationKeyEventContent struct {
// An opaque identifier for the verification process. Must be the same as the one used for the m.key.verification.start message.
TransactionID string `json:"transaction_id,omitempty"`
// The device's ephemeral public key, encoded as unpadded base64.
Key string `json:"key"`
// The user that the event is sent to for in-room verification.
To id.UserID `json:"to,omitempty"`
// Original event ID for in-room verification.
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
}
func (vkec *VerificationKeyEventContent) GetRelatesTo() *RelatesTo {
if vkec.RelatesTo == nil {
vkec.RelatesTo = &RelatesTo{}
}
return vkec.RelatesTo
}
func (vkec *VerificationKeyEventContent) OptionalGetRelatesTo() *RelatesTo {
return vkec.RelatesTo
}
func (vkec *VerificationKeyEventContent) SetRelatesTo(rel *RelatesTo) {
vkec.RelatesTo = rel
}
// VerificationMacEventContent represents the content of a m.key.verification.mac to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationmac
type VerificationMacEventContent struct {
// An opaque identifier for the verification process. Must be the same as the one used for the m.key.verification.start message.
TransactionID string `json:"transaction_id,omitempty"`
// A map of the key ID to the MAC of the key, using the algorithm in the verification process. The MAC is encoded as unpadded base64.
Mac map[id.KeyID]string `json:"mac"`
// The MAC of the comma-separated, sorted, list of key IDs given in the mac property, encoded as unpadded base64.
Keys string `json:"keys"`
// The user that the event is sent to for in-room verification.
To id.UserID `json:"to,omitempty"`
// Original event ID for in-room verification.
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
}
func (vmec *VerificationMacEventContent) GetRelatesTo() *RelatesTo {
if vmec.RelatesTo == nil {
vmec.RelatesTo = &RelatesTo{}
}
return vmec.RelatesTo
}
func (vmec *VerificationMacEventContent) OptionalGetRelatesTo() *RelatesTo {
return vmec.RelatesTo
}
func (vmec *VerificationMacEventContent) SetRelatesTo(rel *RelatesTo) {
vmec.RelatesTo = rel
// VerificationDoneEventContent represents the content of an
// [m.key.verification.done] event (both the to-device and the in-room version)
// as described in [Section 11.12.2.1] of the Spec.
//
// This type is an alias for [VerificationRelatable] since there are no
// additional fields defined by the spec.
//
// [m.key.verification.done]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
type VerificationDoneEventContent struct {
ToDeviceVerificationEvent
InRoomVerificationEvent
}
type VerificationCancelCode string
const (
VerificationCancelByUser VerificationCancelCode = "m.user"
VerificationCancelByTimeout VerificationCancelCode = "m.timeout"
VerificationCancelUnknownTransaction VerificationCancelCode = "m.unknown_transaction"
VerificationCancelUnknownMethod VerificationCancelCode = "m.unknown_method"
VerificationCancelUnexpectedMessage VerificationCancelCode = "m.unexpected_message"
VerificationCancelKeyMismatch VerificationCancelCode = "m.key_mismatch"
VerificationCancelUserMismatch VerificationCancelCode = "m.user_mismatch"
VerificationCancelInvalidMessage VerificationCancelCode = "m.invalid_message"
VerificationCancelAccepted VerificationCancelCode = "m.accepted"
VerificationCancelSASMismatch VerificationCancelCode = "m.mismatched_sas"
VerificationCancelCommitmentMismatch VerificationCancelCode = "m.mismatched_commitment"
VerificationCancelCodeUser VerificationCancelCode = "m.user"
VerificationCancelCodeTimeout VerificationCancelCode = "m.timeout"
VerificationCancelCodeUnknownTransaction VerificationCancelCode = "m.unknown_transaction"
VerificationCancelCodeUnknownMethod VerificationCancelCode = "m.unknown_method"
VerificationCancelCodeUnexpectedMessage VerificationCancelCode = "m.unexpected_message"
VerificationCancelCodeKeyMismatch VerificationCancelCode = "m.key_mismatch"
VerificationCancelCodeUserMismatch VerificationCancelCode = "m.user_mismatch"
VerificationCancelCodeInvalidMessage VerificationCancelCode = "m.invalid_message"
VerificationCancelCodeAccepted VerificationCancelCode = "m.accepted"
VerificationCancelCodeSASMismatch VerificationCancelCode = "m.mismatched_sas"
VerificationCancelCodeCommitmentMismatch VerificationCancelCode = "m.mismatched_commitment"
)
// VerificationCancelEventContent represents the content of a m.key.verification.cancel to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mkeyverificationcancel
// VerificationCancelEventContent represents the content of an
// [m.key.verification.cancel] event (both the to-device and the in-room
// version) as described in [Section 11.12.2.1] of the Spec.
//
// [m.key.verification.cancel]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationcancel
// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
type VerificationCancelEventContent struct {
// The opaque identifier for the verification process/request.
TransactionID string `json:"transaction_id,omitempty"`
// A human readable description of the code. The client should only rely on this string if it does not understand the code.
Reason string `json:"reason"`
// The error code for why the process/request was cancelled by the user.
ToDeviceVerificationEvent
InRoomVerificationEvent
// Code is the error code for why the process/request was cancelled by the
// user.
Code VerificationCancelCode `json:"code"`
// The user that the event is sent to for in-room verification.
To id.UserID `json:"to,omitempty"`
// Original event ID for in-room verification.
RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
// Reason is a human readable description of the code. The client should
// only rely on this string if it does not understand the code.
Reason string `json:"reason"`
}
func (vcec *VerificationCancelEventContent) GetRelatesTo() *RelatesTo {
if vcec.RelatesTo == nil {
vcec.RelatesTo = &RelatesTo{}
}
return vcec.RelatesTo
// VerificationAcceptEventContent represents the content of an
// [m.key.verification.accept] event (both the to-device and the in-room
// version) as described in [Section 11.12.2.2.2] of the Spec.
//
// [m.key.verification.accept]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationaccept
// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
type VerificationAcceptEventContent struct {
ToDeviceVerificationEvent
InRoomVerificationEvent
// Commitment is the hash of the concatenation of the device's ephemeral
// public key (encoded as unpadded base64) and the canonical JSON
// representation of the m.key.verification.start message.
Commitment jsonbytes.UnpaddedBytes `json:"commitment"`
// Hash is the hash method the device is choosing to use, out of the
// options in the m.key.verification.start message.
Hash VerificationHashMethod `json:"hash"`
// KeyAgreementProtocol is the key agreement protocol the device is
// choosing to use, out of the options in the m.key.verification.start
// message.
KeyAgreementProtocol KeyAgreementProtocol `json:"key_agreement_protocol"`
// MessageAuthenticationCode is the message authentication code the device
// is choosing to use, out of the options in the m.key.verification.start
// message.
MessageAuthenticationCode MACMethod `json:"message_authentication_code"`
// ShortAuthenticationString is a list of SAS methods both devices involved
// in the verification process understand. Must be a subset of the options
// in the m.key.verification.start message.
ShortAuthenticationString []SASMethod `json:"short_authentication_string"`
}
func (vcec *VerificationCancelEventContent) OptionalGetRelatesTo() *RelatesTo {
return vcec.RelatesTo
// VerificationKeyEventContent represents the content of an
// [m.key.verification.key] event (both the to-device and the in-room version)
// as described in [Section 11.12.2.2.2] of the Spec.
//
// [m.key.verification.key]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationkey
// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
type VerificationKeyEventContent struct {
ToDeviceVerificationEvent
InRoomVerificationEvent
// Key is the devices ephemeral public key.
Key jsonbytes.UnpaddedBytes `json:"key"`
}
func (vcec *VerificationCancelEventContent) SetRelatesTo(rel *RelatesTo) {
vcec.RelatesTo = rel
// VerificationMACEventContent represents the content of an
// [m.key.verification.mac] event (both the to-device and the in-room version)
// as described in [Section 11.12.2.2.2] of the Spec.
//
// [m.key.verification.mac]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationmac
// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
type VerificationMACEventContent struct {
ToDeviceVerificationEvent
InRoomVerificationEvent
// Keys is the MAC of the comma-separated, sorted, list of key IDs given in
// the MAC property.
Keys jsonbytes.UnpaddedBytes `json:"keys"`
// MAC is a map of the key ID to the MAC of the key, using the algorithm in
// the verification process.
MAC map[id.KeyID]jsonbytes.UnpaddedBytes `json:"mac"`
}

View File

@@ -7,8 +7,11 @@
package id
import (
"encoding/base64"
"fmt"
"strings"
"go.mau.fi/util/random"
)
// OlmMsgType is an Olm message type
@@ -44,6 +47,19 @@ const (
XSUsageUserSigning CrossSigningUsage = "user_signing"
)
type KeyBackupAlgorithm string
const (
KeyBackupAlgorithmMegolmBackupV1 KeyBackupAlgorithm = "m.megolm_backup.v1.curve25519-aes-sha2"
)
// BackupVersion is an arbitrary string that identifies a server side key backup.
type KeyBackupVersion string
func (version KeyBackupVersion) String() string {
return string(version)
}
// A SessionID is an arbitrary string that identifies an Olm or Megolm session.
type SessionID string
@@ -59,6 +75,12 @@ func (ed25519 Ed25519) String() string {
return string(ed25519)
}
func (ed25519 Ed25519) Bytes() []byte {
val, _ := base64.RawStdEncoding.DecodeString(string(ed25519))
// TODO handle errors
return val
}
func (ed25519 Ed25519) Fingerprint() string {
spacedSigningKey := make([]byte, len(ed25519)+(len(ed25519)-1)/4)
var ptr = 0
@@ -82,6 +104,12 @@ func (curve25519 Curve25519) String() string {
return string(curve25519)
}
func (curve25519 Curve25519) Bytes() []byte {
val, _ := base64.RawStdEncoding.DecodeString(string(curve25519))
// TODO handle errors
return val
}
// A DeviceID is an arbitrary string that references a specific device.
type DeviceID string
@@ -147,3 +175,29 @@ type CrossSigningKey struct {
Key Ed25519
First Ed25519
}
// Secret storage keys
type Secret string
func (s Secret) String() string {
return string(s)
}
const (
SecretXSMaster Secret = "m.cross_signing.master"
SecretXSSelfSigning Secret = "m.cross_signing.self_signing"
SecretXSUserSigning Secret = "m.cross_signing.user_signing"
SecretMegolmBackupV1 Secret = "m.megolm_backup.v1"
)
// VerificationTransactionID is a unique identifier for a verification
// transaction.
type VerificationTransactionID string
func NewVerificationTransactionID() VerificationTransactionID {
return VerificationTransactionID(random.String(32))
}
func (t VerificationTransactionID) String() string {
return string(t)
}

View File

@@ -36,19 +36,34 @@ var (
ErrEmptyLocalpart = errors.New("empty localparts are not allowed")
)
// ParseCommonIdentifier parses a common identifier according to https://spec.matrix.org/v1.9/appendices/#common-identifier-format
func ParseCommonIdentifier[Stringish ~string](identifier Stringish) (sigil byte, localpart, homeserver string) {
if len(identifier) == 0 {
return
}
sigil = identifier[0]
strIdentifier := string(identifier)
if strings.ContainsRune(strIdentifier, ':') {
parts := strings.SplitN(strIdentifier, ":", 2)
localpart = parts[0][1:]
homeserver = parts[1]
} else {
localpart = strIdentifier[1:]
}
return
}
// Parse parses the user ID into the localpart and server name.
//
// Note that this only enforces very basic user ID formatting requirements: user IDs start with
// a @, and contain a : after the @. If you want to enforce localpart validity, see the
// ParseAndValidate and ValidateUserLocalpart functions.
func (userID UserID) Parse() (localpart, homeserver string, err error) {
if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(string(userID), ':') {
// This error wrapping lets you use errors.Is() nicely even though the message contains the user ID
var sigil byte
sigil, localpart, homeserver = ParseCommonIdentifier(userID)
if sigil != '@' || homeserver == "" {
err = fmt.Errorf("'%s' %w", userID, ErrInvalidUserID)
return
}
parts := strings.SplitN(string(userID), ":", 2)
localpart, homeserver = strings.TrimPrefix(parts[0], "@"), parts[1]
return
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"strconv"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
@@ -96,8 +97,9 @@ type ReqUIAuthFallback struct {
type ReqUIAuthLogin struct {
BaseAuthData
User string `json:"user"`
Password string `json:"password"`
User string `json:"user,omitempty"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
}
// ReqCreateRoom is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom
@@ -184,11 +186,11 @@ type ReqAliasCreate struct {
}
type OneTimeKey struct {
Key id.Curve25519 `json:"key"`
Fallback bool `json:"fallback,omitempty"`
Signatures Signatures `json:"signatures,omitempty"`
Unsigned map[string]any `json:"unsigned,omitempty"`
IsSigned bool `json:"-"`
Key id.Curve25519 `json:"key"`
Fallback bool `json:"fallback,omitempty"`
Signatures signatures.Signatures `json:"signatures,omitempty"`
Unsigned map[string]any `json:"unsigned,omitempty"`
IsSigned bool `json:"-"`
// Raw data in the one-time key. This must be used for signature verification to ensure unrecognized fields
// aren't thrown away (because that would invalidate the signature).
@@ -230,7 +232,7 @@ type ReqKeysSignatures struct {
Algorithms []id.Algorithm `json:"algorithms,omitempty"`
Usage []id.CrossSigningUsage `json:"usage,omitempty"`
Keys map[id.KeyID]string `json:"keys"`
Signatures Signatures `json:"signatures"`
Signatures signatures.Signatures `json:"signatures"`
}
type ReqUploadSignatures map[id.UserID]map[string]ReqKeysSignatures
@@ -240,15 +242,15 @@ type DeviceKeys struct {
DeviceID id.DeviceID `json:"device_id"`
Algorithms []id.Algorithm `json:"algorithms"`
Keys KeyMap `json:"keys"`
Signatures Signatures `json:"signatures"`
Signatures signatures.Signatures `json:"signatures"`
Unsigned map[string]interface{} `json:"unsigned,omitempty"`
}
type CrossSigningKeys struct {
UserID id.UserID `json:"user_id"`
Usage []id.CrossSigningUsage `json:"usage"`
Keys map[id.KeyID]id.Ed25519 `json:"keys"`
Signatures map[id.UserID]map[id.KeyID]string `json:"signatures,omitempty"`
UserID id.UserID `json:"user_id"`
Usage []id.CrossSigningUsage `json:"usage"`
Keys map[id.KeyID]id.Ed25519 `json:"keys"`
Signatures signatures.Signatures `json:"signatures,omitempty"`
}
func (csk *CrossSigningKeys) FirstKey() id.Ed25519 {
@@ -283,8 +285,6 @@ func (km KeyMap) GetCurve25519(deviceID id.DeviceID) id.Curve25519 {
return id.Curve25519(val)
}
type Signatures map[id.UserID]map[id.KeyID]string
type ReqQueryKeys struct {
DeviceKeys DeviceKeysRequest `json:"device_keys"`
Timeout int64 `json:"timeout,omitempty"`
@@ -429,20 +429,26 @@ type ReqBeeperSplitRoom struct {
Parts []BeeperSplitRoomPart `json:"parts"`
}
type ReqRoomKeysVersionCreate struct {
Algorithm string `json:"algorithm"`
AuthData json.RawMessage `json:"auth_data"`
type ReqRoomKeysVersionCreate[A any] struct {
Algorithm id.KeyBackupAlgorithm `json:"algorithm"`
AuthData A `json:"auth_data"`
}
type ReqRoomKeysUpdate struct {
Rooms map[id.RoomID]ReqRoomKeysRoomUpdate `json:"rooms"`
type ReqRoomKeysVersionUpdate[A any] struct {
Algorithm id.KeyBackupAlgorithm `json:"algorithm"`
AuthData A `json:"auth_data"`
Version id.KeyBackupVersion `json:"version,omitempty"`
}
type ReqRoomKeysRoomUpdate struct {
Sessions map[id.SessionID]ReqRoomKeysSessionUpdate `json:"sessions"`
type ReqKeyBackup struct {
Rooms map[id.RoomID]ReqRoomKeyBackup `json:"rooms"`
}
type ReqRoomKeysSessionUpdate struct {
type ReqRoomKeyBackup struct {
Sessions map[id.SessionID]ReqKeyBackupData `json:"sessions"`
}
type ReqKeyBackupData struct {
FirstMessageIndex int `json:"first_message_index"`
ForwardedCount int `json:"forwarded_count"`
IsVerified bool `json:"is_verified"`

View File

@@ -118,19 +118,7 @@ type RespCreateMXC struct {
}
// RespPreviewURL is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixmediav3preview_url
type RespPreviewURL struct {
CanonicalURL string `json:"og:url,omitempty"`
Title string `json:"og:title,omitempty"`
Type string `json:"og:type,omitempty"`
Description string `json:"og:description,omitempty"`
ImageURL id.ContentURIString `json:"og:image,omitempty"`
ImageSize int `json:"matrix:image:size,omitempty"`
ImageWidth int `json:"og:image:width,omitempty"`
ImageHeight int `json:"og:image:height,omitempty"`
ImageType string `json:"og:image:type,omitempty"`
}
type RespPreviewURL = event.LinkPreview
// RespUserInteractive is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
type RespUserInteractive struct {
@@ -321,6 +309,12 @@ func (slr SyncLeftRoom) MarshalJSON() ([]byte, error) {
return marshalAndDeleteEmpty((marshalableSyncLeftRoom)(slr), syncLeftRoomPathsToDelete)
}
type BeeperInboxPreviewEvent struct {
EventID id.EventID `json:"event_id"`
Timestamp jsontime.UnixMilli `json:"origin_server_ts"`
Event *event.Event `json:"event,omitempty"`
}
type SyncJoinedRoom struct {
Summary LazyLoadSummary `json:"summary"`
State SyncEventsList `json:"state"`
@@ -331,6 +325,8 @@ type SyncJoinedRoom struct {
UnreadNotifications *UnreadNotificationCounts `json:"unread_notifications,omitempty"`
// https://github.com/matrix-org/matrix-spec-proposals/pull/2654
MSC2654UnreadCount *int `json:"org.matrix.msc2654.unread_count,omitempty"`
// Beeper extension
BeeperInboxPreview *BeeperInboxPreviewEvent `json:"com.beeper.inbox.preview,omitempty"`
}
type UnreadNotificationCounts struct {
@@ -593,30 +589,30 @@ type RespTimestampToEvent struct {
}
type RespRoomKeysVersionCreate struct {
Version string `json:"version"`
Version id.KeyBackupVersion `json:"version"`
}
type RespRoomKeysVersion struct {
Algorithm string `json:"algorithm"`
AuthData json.RawMessage `json:"auth_data"`
Count int `json:"count"`
ETag string `json:"etag"`
Version string `json:"version"`
type RespRoomKeysVersion[A any] struct {
Algorithm id.KeyBackupAlgorithm `json:"algorithm"`
AuthData A `json:"auth_data"`
Count int `json:"count"`
ETag string `json:"etag"`
Version id.KeyBackupVersion `json:"version"`
}
type RespRoomKeys struct {
Rooms map[id.RoomID]RespRoomKeysRoom `json:"rooms"`
type RespRoomKeys[S any] struct {
Rooms map[id.RoomID]RespRoomKeyBackup[S] `json:"rooms"`
}
type RespRoomKeysRoom struct {
Sessions map[id.SessionID]RespRoomKeysSession `json:"sessions"`
type RespRoomKeyBackup[S any] struct {
Sessions map[id.SessionID]RespKeyBackupData[S] `json:"sessions"`
}
type RespRoomKeysSession struct {
FirstMessageIndex int `json:"first_message_index"`
ForwardedCount int `json:"forwarded_count"`
IsVerified bool `json:"is_verified"`
SessionData json.RawMessage `json:"session_data"`
type RespKeyBackupData[S any] struct {
FirstMessageIndex int `json:"first_message_index"`
ForwardedCount int `json:"forwarded_count"`
IsVerified bool `json:"is_verified"`
SessionData S `json:"session_data"`
}
type RespRoomKeysUpdate struct {

View File

@@ -7,7 +7,7 @@ import (
"strings"
)
const Version = "v0.17.0"
const Version = "v0.18.0"
var GoModVersion = ""
var Commit = ""