Merge branch 'master' into cross-sign

This commit is contained in:
Tulir Asokan 2020-10-27 00:38:52 +02:00
commit 495d77b3a0
29 changed files with 465 additions and 166 deletions

View file

@ -1,11 +1,23 @@
# mautrix-go
[![GoDoc](https://godoc.org/maunium.net/go/mautrix?status.svg)](https://godoc.org/maunium.net/go/mautrix)
A Golang Matrix framework.
A Golang Matrix framework. Used by [gomuks](https://matrix.org/docs/projects/client/gomuks),
[go-neb](https://github.com/matrix-org/go-neb), [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp)
and others.
Matrix room: [`#maunium:maunium.net`](https://matrix.to/#/#maunium:maunium.net)
This project is based on [matrix-org/gomatrix](https://github.com/matrix-org/gomatrix).
The original project is licensed under [Apache 2.0](https://github.com/matrix-org/gomatrix/blob/master/LICENSE).
In addition to the basic client API features the original project has, this framework also has:
* Appservice support (Intent API like mautrix-python, room state storage, etc)
* End-to-end encryption support (incl. interactive SAS verification)
* Structs for parsing event content
* Helpers for parsing and generating Matrix HTML
* Helpers for handling push rules
This project contains modules that are licensed under Apache 2.0:
* [maunium.net/go/mautrix/crypto/canonicaljson](crypto/canonicaljson)

View file

@ -11,6 +11,7 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
@ -51,27 +52,38 @@ func (as *AppService) Stop() {
return
}
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = as.server.Shutdown(ctx)
as.server = nil
}
// CheckServerToken checks if the given request originated from the Matrix homeserver.
func (as *AppService) CheckServerToken(w http.ResponseWriter, r *http.Request) bool {
query := r.URL.Query()
val, ok := query["access_token"]
if !ok {
func (as *AppService) CheckServerToken(w http.ResponseWriter, r *http.Request) (isValid bool) {
authHeader := r.Header.Get("Authorization")
if len(authHeader) > 0 && strings.HasPrefix(authHeader, "Bearer ") {
isValid = authHeader[len("Bearer "):] == as.Registration.ServerToken
} else {
queryToken := r.URL.Query().Get("access_token")
if len(queryToken) > 0 {
isValid = queryToken == as.Registration.ServerToken
} else {
Error{
ErrorCode: ErrUnknownToken,
HTTPStatus: http.StatusForbidden,
Message: "Missing access token",
}.Write(w)
return
}
}
if !isValid {
Error{
ErrorCode: ErrForbidden,
ErrorCode: ErrUnknownToken,
HTTPStatus: http.StatusForbidden,
Message: "Bad token supplied.",
Message: "Incorrect access token",
}.Write(w)
return false
}
for _, str := range val {
return str == as.Registration.ServerToken
}
return false
return
}
// PutTransaction handles a /transactions PUT call from the homeserver.
@ -86,7 +98,7 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
Error{
ErrorCode: ErrNoTransactionID,
HTTPStatus: http.StatusBadRequest,
Message: "Missing transaction ID.",
Message: "Missing transaction ID",
}.Write(w)
return
}
@ -94,9 +106,9 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil || len(body) == 0 {
Error{
ErrorCode: ErrNoBody,
ErrorCode: ErrNotJSON,
HTTPStatus: http.StatusBadRequest,
Message: "Missing request body.",
Message: "Missing request body",
}.Write(w)
return
}
@ -111,11 +123,19 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
if err != nil {
as.Log.Warnfln("Failed to parse JSON of transaction %s: %v", txnID, err)
Error{
ErrorCode: ErrInvalidJSON,
ErrorCode: ErrBadJSON,
HTTPStatus: http.StatusBadRequest,
Message: "Failed to parse body JSON.",
Message: "Failed to parse body JSON",
}.Write(w)
} else {
if as.Registration.EphemeralEvents {
if eventList.EphemeralEvents != nil {
as.handleEvents(eventList.EphemeralEvents, event.EphemeralEventType)
} else if eventList.SoruEphemeralEvents != nil {
as.handleEvents(eventList.SoruEphemeralEvents, event.EphemeralEventType)
}
}
as.handleEvents(eventList.Events, event.UnknownEventType)
for _, evt := range eventList.Events {
if evt.StateKey != nil {
evt.Type.Class = event.StateEventType
@ -134,6 +154,30 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
as.lastProcessedTransaction = txnID
}
func (as *AppService) handleEvents(evts []*event.Event, typeClass event.TypeClass) {
for _, evt := range evts {
if typeClass != event.UnknownEventType {
evt.Type.Class = typeClass
} else if evt.StateKey != nil {
evt.Type.Class = event.StateEventType
} else {
evt.Type.Class = event.MessageEventType
}
err := evt.Content.ParseRaw(evt.Type)
if err != nil {
if evt.ID != "" {
as.Log.Debugfln("Failed to parse content of %s (%s): %v", evt.ID, evt.Type.Type, err)
} else {
as.Log.Debugfln("Failed to parse content of a %s: %v", evt.Type.Type, err)
}
}
if evt.Type.IsState() {
as.UpdateState(evt)
}
as.Events <- evt
}
}
// GetRoom handles a /rooms GET call from the homeserver.
func (as *AppService) GetRoom(w http.ResponseWriter, r *http.Request) {
if !as.CheckServerToken(w, r) {

View file

@ -15,7 +15,9 @@ import (
// EventList contains a list of events.
type EventList struct {
Events []*event.Event `json:"events"`
Events []*event.Event `json:"events"`
EphemeralEvents []*event.Event `json:"ephemeral"`
SoruEphemeralEvents []*event.Event `json:"de.sorunome.msc2409.ephemeral"`
}
// EventListener is a function that receives events.
@ -23,12 +25,14 @@ type EventListener func(evt *event.Event)
// WriteBlankOK writes a blank OK message as a reply to a HTTP request.
func WriteBlankOK(w http.ResponseWriter) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("{}"))
}
// Respond responds to a HTTP request with a JSON object.
func Respond(w http.ResponseWriter, data interface{}) error {
w.Header().Add("Content-Type", "application/json")
dataStr, err := json.Marshal(data)
if err != nil {
return err
@ -45,6 +49,7 @@ type Error struct {
}
func (err Error) Write(w http.ResponseWriter) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(err.HTTPStatus)
_ = Respond(w, &err)
}
@ -54,13 +59,13 @@ type ErrorCode string
// Native ErrorCodes
const (
ErrForbidden ErrorCode = "M_FORBIDDEN"
ErrUnknown ErrorCode = "M_UNKNOWN"
ErrUnknownToken ErrorCode = "M_UNKNOWN_TOKEN"
ErrBadJSON ErrorCode = "M_BAD_JSON"
ErrNotJSON ErrorCode = "M_NOT_JSON"
ErrUnknown ErrorCode = "M_UNKNOWN"
)
// Custom ErrorCodes
const (
ErrNoTransactionID ErrorCode = "NET.MAUNIUM.NO_TRANSACTION_ID"
ErrNoBody ErrorCode = "NET.MAUNIUM.NO_REQUEST_BODY"
ErrInvalidJSON ErrorCode = "NET.MAUNIUM.INVALID_JSON"
)

View file

@ -23,6 +23,7 @@ type Registration struct {
SenderLocalpart string `yaml:"sender_localpart"`
RateLimited bool `yaml:"rate_limited"`
Namespaces Namespaces `yaml:"namespaces"`
EphemeralEvents bool `yaml:"de.sorunome.msc2409.push_ephemeral,omitempty"`
}
// CreateRegistration creates a Registration with random appservice and homeserver tokens.

View file

@ -8,8 +8,8 @@ package crypto
import (
"encoding/json"
"github.com/pkg/errors"
"errors"
"fmt"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@ -18,7 +18,7 @@ import (
var (
IncorrectEncryptedContentType = errors.New("event content is not instance of *event.EncryptedEventContent")
NoSessionFound = errors.New("failed to decrypt megolm event: no session with given ID found")
DuplicateMessageIndex = errors.New("duplicate message index")
DuplicateMessageIndex = errors.New("duplicate megolm message index")
WrongRoom = errors.New("encrypted megolm event is not intended for this room")
DeviceKeyMismatch = errors.New("device keys in event and verified device info do not match")
)
@ -39,14 +39,14 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro
}
sess, err := mach.CryptoStore.GetGroupSession(evt.RoomID, content.SenderKey, content.SessionID)
if err != nil {
return nil, errors.Wrap(err, "failed to get group session")
return nil, fmt.Errorf("failed to get group session: %w", err)
} else if sess == nil {
mach.checkIfWedged(evt)
return nil, NoSessionFound
return nil, fmt.Errorf("%w (ID %s)", NoSessionFound, content.SessionID)
}
plaintext, messageIndex, err := sess.Internal.Decrypt(content.MegolmCiphertext)
if err != nil {
return nil, errors.Wrap(err, "failed to decrypt megolm event")
return nil, fmt.Errorf("failed to decrypt megolm event: %w", err)
} else if !mach.CryptoStore.ValidateMessageIndex(content.SenderKey, content.SessionID, evt.ID, messageIndex, evt.Timestamp) {
return nil, DuplicateMessageIndex
}
@ -72,7 +72,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro
megolmEvt := &megolmEvent{}
err = json.Unmarshal(plaintext, &megolmEvt)
if err != nil {
return nil, errors.Wrap(err, "failed to parse megolm payload")
return nil, fmt.Errorf("failed to parse megolm payload: %w", err)
} else if megolmEvt.RoomID != evt.RoomID {
return nil, WrongRoom
}
@ -82,7 +82,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro
if event.IsUnsupportedContentType(err) {
mach.Log.Warn("Unsupported event type %s in encrypted event %s", megolmEvt.Type.Repr(), evt.ID)
} else {
return nil, errors.Wrap(err, "failed to parse content of megolm payload event")
return nil, fmt.Errorf("failed to parse content of megolm payload event: %w", err)
}
}
if content.RelatesTo != nil {

View file

@ -8,8 +8,8 @@ package crypto
import (
"encoding/json"
"github.com/pkg/errors"
"errors"
"fmt"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@ -76,7 +76,7 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic
mach.Log.Warn("Found matching session yet decryption failed for sender %s with key %s", sender, senderKey)
mach.markDeviceForUnwedging(sender, senderKey)
}
return nil, errors.Wrap(err, "failed to decrypt olm event")
return nil, fmt.Errorf("failed to decrypt olm event: %w", err)
}
// Decryption failed with every known session or no known sessions, let's try to create a new session.
@ -92,13 +92,13 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic
session, err := mach.createInboundSession(senderKey, ciphertext)
if err != nil {
mach.markDeviceForUnwedging(sender, senderKey)
return nil, errors.Wrap(err, "failed to create new session from prekey message")
return nil, fmt.Errorf("failed to create new session from prekey message: %w", err)
}
mach.Log.Trace("Created inbound session %s for %s/%s (sender key: %s)", session.ID(), sender, deviceID, senderKey)
mach.Log.Debug("Created inbound olm session %s for %s/%s (sender key: %s)", session.ID(), sender, deviceID, senderKey)
plaintext, err = session.Decrypt(ciphertext, olmType)
if err != nil {
return nil, errors.Wrap(err, "failed to decrypt olm event with session created from prekey message")
return nil, fmt.Errorf("failed to decrypt olm event with session created from prekey message: %w", err)
}
err = mach.CryptoStore.UpdateSession(senderKey, session)
@ -110,7 +110,7 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic
var olmEvt DecryptedOlmEvent
err = json.Unmarshal(plaintext, &olmEvt)
if err != nil {
return nil, errors.Wrap(err, "failed to parse olm payload")
return nil, fmt.Errorf("failed to parse olm payload: %w", err)
}
if sender != olmEvt.Sender {
return nil, SenderMismatch
@ -122,7 +122,7 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic
err = olmEvt.Content.ParseRaw(olmEvt.Type)
if err != nil && !event.IsUnsupportedContentType(err) {
return nil, errors.Wrap(err, "failed to parse content of olm payload event")
return nil, fmt.Errorf("failed to parse content of olm payload event: %w", err)
}
olmEvt.SenderKey = senderKey
@ -133,13 +133,13 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic
func (mach *OlmMachine) tryDecryptOlmCiphertext(senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) ([]byte, error) {
sessions, err := mach.CryptoStore.GetSessions(senderKey)
if err != nil {
return nil, errors.Wrapf(err, "failed to get session for %s", senderKey)
return nil, fmt.Errorf("failed to get session for %s: %w", senderKey, err)
}
for _, session := range sessions {
if olmType == id.OlmMsgTypePreKey {
matches, err := session.Internal.MatchesInboundSession(ciphertext)
if err != nil {
return nil, errors.Wrap(err, "failed to check if ciphertext matches inbound session")
return nil, fmt.Errorf("failed to check if ciphertext matches inbound session: %w", err)
} else if !matches {
continue
}

View file

@ -7,7 +7,8 @@
package crypto
import (
"github.com/pkg/errors"
"errors"
"fmt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
@ -163,7 +164,7 @@ func (mach *OlmMachine) validateDevice(userID id.UserID, deviceID id.DeviceID, d
ok, err := olm.VerifySignatureJSON(deviceKeys, userID, deviceID.String(), signingKey)
if err != nil {
return existing, errors.Wrap(err, "failed to verify signature")
return existing, fmt.Errorf("failed to verify signature: %w", err)
} else if !ok {
return existing, InvalidKeySignature
}

View file

@ -8,8 +8,8 @@ package crypto
import (
"encoding/json"
"github.com/pkg/errors"
"errors"
"fmt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
@ -52,7 +52,7 @@ func (mach *OlmMachine) EncryptMegolmEvent(roomID id.RoomID, evtType event.Type,
mach.Log.Trace("Encrypting event of type %s for %s", evtType.Type, roomID)
session, err := mach.CryptoStore.GetOutboundGroupSession(roomID)
if err != nil {
return nil, errors.Wrap(err, "failed to get outbound group session")
return nil, fmt.Errorf("failed to get outbound group session: %w", err)
} else if session == nil {
return nil, NoGroupSession
}
@ -89,6 +89,11 @@ func (mach *OlmMachine) newOutboundGroupSession(roomID id.RoomID) *OutboundGroup
return session
}
type deviceSessionWrapper struct {
session *OlmSession
identity *DeviceIdentity
}
// ShareGroupSession shares a group session for a specific room with all the devices of the given user list.
//
// For devices with TrustStateBlacklisted, a m.room_key.withheld event with code=m.blacklisted is sent.
@ -97,7 +102,7 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e
mach.Log.Debug("Sharing group session for room %s to %v", roomID, users)
session, err := mach.CryptoStore.GetOutboundGroupSession(roomID)
if err != nil {
return errors.Wrap(err, "failed to get previous outbound group session")
return fmt.Errorf("failed to get previous outbound group session: %w", err)
} else if session != nil && session.Shared && !session.Expired() {
return AlreadyShared
}
@ -105,8 +110,9 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e
session = mach.newOutboundGroupSession(roomID)
}
toDevice := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)}
withheldCount := 0
toDeviceWithheld := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)}
olmSessions := make(map[id.UserID]map[id.DeviceID]deviceSessionWrapper)
missingSessions := make(map[id.UserID]map[id.DeviceID]*DeviceIdentity)
missingUserSessions := make(map[id.DeviceID]*DeviceIdentity)
var fetchKeys []id.UserID
@ -122,9 +128,10 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e
mach.Log.Trace("%s has no devices, skipping", userID)
} else {
mach.Log.Trace("Trying to encrypt group session %s for %s", session.ID(), userID)
toDevice.Messages[userID] = make(map[id.DeviceID]*event.Content)
toDeviceWithheld.Messages[userID] = make(map[id.DeviceID]*event.Content)
mach.encryptGroupSessionForUser(session, userID, devices, toDevice.Messages[userID], toDeviceWithheld.Messages[userID], missingUserSessions)
olmSessions[userID] = make(map[id.DeviceID]deviceSessionWrapper)
mach.findOlmSessionsForUser(session, userID, devices, olmSessions[userID], toDeviceWithheld.Messages[userID], missingUserSessions)
withheldCount += len(toDeviceWithheld.Messages[userID])
if len(missingUserSessions) > 0 {
missingSessions[userID] = missingUserSessions
missingUserSessions = make(map[id.DeviceID]*DeviceIdentity)
@ -132,9 +139,6 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e
if len(toDeviceWithheld.Messages[userID]) == 0 {
delete(toDeviceWithheld.Messages, userID)
}
if len(toDevice.Messages[userID]) == 0 {
delete(toDevice.Messages, userID)
}
}
}
@ -146,10 +150,12 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e
}
}
mach.Log.Trace("Creating missing outbound sessions")
err = mach.createOutboundSessions(missingSessions)
if err != nil {
mach.Log.Error("Failed to create missing outbound sessions: %v", err)
if len(missingSessions) > 0 {
mach.Log.Trace("Creating missing outbound sessions")
err = mach.createOutboundSessions(missingSessions)
if err != nil {
mach.Log.Error("Failed to create missing outbound sessions: %v", err)
}
}
for userID, devices := range missingSessions {
@ -157,10 +163,10 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e
// No missing sessions
continue
}
output, ok := toDevice.Messages[userID]
output, ok := olmSessions[userID]
if !ok {
output = make(map[id.DeviceID]*event.Content)
toDevice.Messages[userID] = output
output = make(map[id.DeviceID]deviceSessionWrapper)
olmSessions[userID] = output
}
withheld, ok := toDeviceWithheld.Messages[userID]
if !ok {
@ -168,35 +174,62 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e
toDeviceWithheld.Messages[userID] = withheld
}
mach.Log.Trace("Trying to encrypt group session %s for %s (post-fetch retry)", session.ID(), userID)
mach.encryptGroupSessionForUser(session, userID, devices, output, withheld, nil)
mach.findOlmSessionsForUser(session, userID, devices, output, withheld, nil)
withheldCount += len(toDeviceWithheld.Messages[userID])
if len(toDeviceWithheld.Messages[userID]) == 0 {
delete(toDeviceWithheld.Messages, userID)
}
if len(toDevice.Messages[userID]) == 0 {
delete(toDevice.Messages, userID)
}
err = mach.encryptAndSendGroupSession(session, olmSessions)
if err != nil {
return fmt.Errorf("failed to share group session: %w", err)
}
if len(toDeviceWithheld.Messages) > 0 {
mach.Log.Trace("Sending to-device messages to %d devices of %d users to report withheld keys in %s", withheldCount, len(toDeviceWithheld.Messages), roomID)
// TODO remove the next 4 lines once clients support m.room_key.withheld
_, err = mach.Client.SendToDevice(event.ToDeviceOrgMatrixRoomKeyWithheld, toDeviceWithheld)
if err != nil {
mach.Log.Warn("Failed to report withheld keys in %s (legacy event type): %v", roomID, err)
}
_, err = mach.Client.SendToDevice(event.ToDeviceRoomKeyWithheld, toDeviceWithheld)
if err != nil {
mach.Log.Warn("Failed to report withheld keys in %s: %v", roomID, err)
}
}
mach.Log.Trace("Sending to-device to %d users to share group session for %s", len(toDevice.Messages), roomID)
_, err = mach.Client.SendToDevice(event.ToDeviceEncrypted, toDevice)
if err != nil {
return errors.Wrap(err, "failed to share group session")
}
mach.Log.Trace("Sending to-device messages to %d users to report withheld keys in %s", len(toDeviceWithheld.Messages), roomID)
// TODO remove the next line once clients support m.room_key.withheld
_, _ = mach.Client.SendToDevice(event.ToDeviceOrgMatrixRoomKeyWithheld, toDeviceWithheld)
_, err = mach.Client.SendToDevice(event.ToDeviceRoomKeyWithheld, toDeviceWithheld)
if err != nil {
mach.Log.Warn("Failed to report withheld keys in %s: %v", roomID, err)
}
mach.Log.Debug("Group session for %s successfully shared", roomID)
mach.Log.Debug("Group session %s for %s successfully shared", session.ID(), roomID)
session.Shared = true
return mach.CryptoStore.AddOutboundGroupSession(session)
}
func (mach *OlmMachine) encryptGroupSessionForUser(session *OutboundGroupSession, userID id.UserID, devices map[id.DeviceID]*DeviceIdentity, output, withheld map[id.DeviceID]*event.Content, missingOutput map[id.DeviceID]*DeviceIdentity) {
func (mach *OlmMachine) encryptAndSendGroupSession(session *OutboundGroupSession, olmSessions map[id.UserID]map[id.DeviceID]deviceSessionWrapper) error {
deviceCount := 0
toDevice := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)}
for userID, sessions := range olmSessions {
if len(sessions) == 0 {
continue
}
output := make(map[id.DeviceID]*event.Content)
toDevice.Messages[userID] = output
for deviceID, device := range sessions {
device.session.Lock()
// We intentionally defer in a loop as it's the safest way of making sure nothing gets locked permanently.
defer device.session.Unlock()
content := mach.encryptOlmEvent(device.session, device.identity, event.ToDeviceRoomKey, session.ShareContent())
output[deviceID] = &event.Content{Parsed: content}
deviceCount++
mach.Log.Trace("Encrypted group session %s for %s of %s", session.ID(), deviceID, userID)
}
}
mach.Log.Trace("Sending to-device to %d devices of %d users to share group session %s", deviceCount, len(toDevice.Messages), session.ID())
_, err := mach.Client.SendToDevice(event.ToDeviceEncrypted, toDevice)
return err
}
func (mach *OlmMachine) findOlmSessionsForUser(session *OutboundGroupSession, userID id.UserID, devices map[id.DeviceID]*DeviceIdentity, output map[id.DeviceID]deviceSessionWrapper, withheld map[id.DeviceID]*event.Content, missingOutput map[id.DeviceID]*DeviceIdentity) {
for deviceID, device := range devices {
userKey := UserDevice{UserID: userID, DeviceID: deviceID}
if state := session.Users[userKey]; state != OGSNotShared {
@ -233,10 +266,11 @@ func (mach *OlmMachine) encryptGroupSessionForUser(session *OutboundGroupSession
missingOutput[deviceID] = device
}
} else {
content := mach.encryptOlmEvent(deviceSession, device, event.ToDeviceRoomKey, session.ShareContent())
output[deviceID] = &event.Content{Parsed: content}
output[deviceID] = deviceSessionWrapper{
session: deviceSession,
identity: device,
}
session.Users[userKey] = OGSAlreadyShared
mach.Log.Trace("Encrypted group session %s for %s of %s", session.ID(), deviceID, userID)
}
}
}

View file

@ -8,8 +8,7 @@ package crypto
import (
"encoding/json"
"github.com/pkg/errors"
"fmt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
@ -69,7 +68,7 @@ func (mach *OlmMachine) createOutboundSessions(input map[id.UserID]map[id.Device
Timeout: 10 * 1000,
})
if err != nil {
return errors.Wrap(err, "failed to claim keys")
return fmt.Errorf("failed to claim keys: %w", err)
}
for userID, user := range resp.OneTimeKeys {
for deviceID, oneTimeKeys := range user {

View file

@ -20,7 +20,6 @@ import (
"fmt"
"math"
"github.com/pkg/errors"
"golang.org/x/crypto/pbkdf2"
"maunium.net/go/mautrix/crypto/olm"
@ -91,7 +90,7 @@ func exportSessions(sessions []*InboundGroupSession) ([]ExportedSession, error)
for i, session := range sessions {
key, err := session.Internal.Export(session.Internal.FirstKnownIndex())
if err != nil {
return nil, errors.Wrap(err, "failed to export session")
return nil, fmt.Errorf("failed to export session: %w", err)
}
export[i] = ExportedSession{
Algorithm: id.AlgorithmMegolmV1,

View file

@ -15,8 +15,8 @@ import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"github.com/pkg/errors"
"errors"
"fmt"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/id"
@ -85,7 +85,7 @@ func decryptKeyExport(passphrase string, exportData []byte) ([]ExportedSession,
var sessionsJSON []ExportedSession
err := json.Unmarshal(unencryptedData, &sessionsJSON)
if err != nil {
return nil, errors.Wrap(err, "invalid export json")
return nil, fmt.Errorf("invalid export json: %w", err)
}
return sessionsJSON, nil
}
@ -97,7 +97,7 @@ func (mach *OlmMachine) importExportedRoomKey(session ExportedSession) (bool, er
igsInternal, err := olm.InboundGroupSessionImport([]byte(session.SessionKey))
if err != nil {
return false, errors.Wrap(err, "failed to import session")
return false, fmt.Errorf("failed to import session: %w", err)
} else if igsInternal.ID() != session.SessionID {
return false, ErrMismatchingExportedSessionID
}
@ -116,8 +116,9 @@ func (mach *OlmMachine) importExportedRoomKey(session ExportedSession) (bool, er
}
err = mach.CryptoStore.PutGroupSession(igs.RoomID, igs.SenderKey, igs.ID(), igs)
if err != nil {
return false, errors.Wrap(err, "failed to store imported session")
return false, fmt.Errorf("failed to store imported session: %w", err)
}
mach.markSessionReceived(igs.ID())
return true, nil
}

View file

@ -134,6 +134,7 @@ func (mach *OlmMachine) importForwardedRoomKey(evt *DecryptedOlmEvent, content *
mach.Log.Error("Failed to store new inbound group session: %v", err)
return false
}
mach.markSessionReceived(content.SessionID)
mach.Log.Trace("Created inbound group session %s/%s/%s", content.RoomID, content.SenderKey, content.SessionID)
return true
}

View file

@ -7,11 +7,11 @@
package crypto
import (
"errors"
"fmt"
"sync"
"time"
"github.com/pkg/errors"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/ssss"
"maunium.net/go/mautrix/id"
@ -52,6 +52,9 @@ type OlmMachine struct {
roomKeyRequestFilled *sync.Map
keyVerificationTransactionState *sync.Map
keyWaiters map[id.SessionID]chan struct{}
keyWaitersLock sync.Mutex
CrossSigningKeys *CrossSigningKeysCache
crossSigningPubkeys *CrossSigningPublicKeysCache
}
@ -86,6 +89,8 @@ func NewOlmMachine(client *mautrix.Client, log Logger, cryptoStore Store, stateS
roomKeyRequestFilled: &sync.Map{},
keyVerificationTransactionState: &sync.Map{},
keyWaiters: make(map[id.SessionID]chan struct{}),
}
mach.AllowKeyShare = mach.defaultAllowKeyShare
return mach
@ -266,7 +271,7 @@ func (mach *OlmMachine) GetOrFetchDevice(userID id.UserID, deviceID id.DeviceID)
// get device identity
device, err := mach.CryptoStore.GetDevice(userID, deviceID)
if err != nil {
return nil, errors.Wrap(err, "failed to get sender device from store")
return nil, fmt.Errorf("failed to get sender device from store: %w", err)
} else if device != nil {
return device, nil
}
@ -276,9 +281,9 @@ func (mach *OlmMachine) GetOrFetchDevice(userID id.UserID, deviceID id.DeviceID)
if device, ok = devices[deviceID]; ok {
return device, nil
}
return nil, errors.Errorf("Failed to get identity for device %v", deviceID)
return nil, fmt.Errorf("didn't get identity for device %s of %s", deviceID, userID)
}
return nil, errors.Errorf("Error fetching devices for user %v", userID)
return nil, fmt.Errorf("didn't get any devices for %s", userID)
}
// SendEncryptedToDevice sends an Olm-encrypted event to the given user device.
@ -298,9 +303,12 @@ func (mach *OlmMachine) SendEncryptedToDevice(device *DeviceIdentity, content ev
return err
}
if olmSess == nil {
return errors.Errorf("Did not find created outbound session for device %v", device.DeviceID)
return fmt.Errorf("didn't find created outbound session for device %s of %s", device.DeviceID, device.UserID)
}
olmSess.Lock()
defer olmSess.Unlock()
encrypted := mach.encryptOlmEvent(olmSess, device, event.ToDeviceForwardedRoomKey, content)
encryptedContent := &event.Content{Parsed: &encrypted}
@ -329,8 +337,40 @@ func (mach *OlmMachine) createGroupSession(senderKey id.SenderKey, signingKey id
err = mach.CryptoStore.PutGroupSession(roomID, senderKey, sessionID, igs)
if err != nil {
mach.Log.Error("Failed to store new inbound group session: %v", err)
return
}
mach.markSessionReceived(sessionID)
mach.Log.Debug("Received inbound group session %s / %s / %s", roomID, senderKey, sessionID)
}
func (mach *OlmMachine) markSessionReceived(id id.SessionID) {
mach.keyWaitersLock.Lock()
ch, ok := mach.keyWaiters[id]
if ok {
close(ch)
delete(mach.keyWaiters, id)
}
mach.keyWaitersLock.Unlock()
}
// WaitForSession waits for the given Megolm session to arrive.
func (mach *OlmMachine) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool {
mach.keyWaitersLock.Lock()
ch, ok := mach.keyWaiters[sessionID]
if !ok {
ch := make(chan struct{})
mach.keyWaiters[sessionID] = ch
}
mach.keyWaitersLock.Unlock()
select {
case <-ch:
return true
case <-time.After(timeout):
sess, err := mach.CryptoStore.GetGroupSession(roomID, senderKey, sessionID)
// Check if the session somehow appeared in the store without telling us
// We accept withheld sessions as received, as then the decryption attempt will show the error.
return sess != nil || errors.Is(err, ErrGroupSessionWithheld)
}
mach.Log.Trace("Created inbound group session %s/%s/%s", roomID, senderKey, sessionID)
}
func (mach *OlmMachine) receiveRoomKey(evt *DecryptedOlmEvent, content *event.RoomKeyEventContent) {

View file

@ -7,11 +7,11 @@
package crypto
import (
"errors"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
@ -43,6 +43,20 @@ type OlmSession struct {
Internal olm.Session
ExpirationMixin
id id.SessionID
// This is unexported so gob wouldn't insist on trying to marshaling it
lock sync.Locker
}
func (session *OlmSession) SetLock(lock sync.Locker) {
session.lock = lock
}
func (session *OlmSession) Lock() {
session.lock.Lock()
}
func (session *OlmSession) Unlock() {
session.lock.Unlock()
}
func (session *OlmSession) ID() id.SessionID {
@ -55,6 +69,7 @@ func (session *OlmSession) ID() id.SessionID {
func wrapSession(session *olm.Session) *OlmSession {
return &OlmSession{
Internal: *session,
lock: &sync.Mutex{},
ExpirationMixin: ExpirationMixin{
TimeMixin: TimeMixin{
CreationTime: time.Now(),

View file

@ -10,9 +10,9 @@ import (
"database/sql"
"fmt"
"strings"
"sync"
"github.com/lib/pq"
"github.com/pkg/errors"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/sql_store_upgrade"
@ -31,6 +31,9 @@ type SQLCryptoStore struct {
SyncToken string
PickleKey []byte
Account *OlmAccount
olmSessionCache map[id.SenderKey]map[id.SessionID]*OlmSession
olmSessionCacheLock sync.Mutex
}
var _ Store = (*SQLCryptoStore)(nil)
@ -45,6 +48,8 @@ func NewSQLCryptoStore(db *sql.DB, dialect string, accountID string, deviceID id
PickleKey: pickleKey,
AccountID: accountID,
DeviceID: deviceID,
olmSessionCache: make(map[id.SenderKey]map[id.SessionID]*OlmSession),
}
}
@ -125,7 +130,12 @@ func (store *SQLCryptoStore) GetAccount() (*OlmAccount, error) {
// HasSession returns whether there is an Olm session for the given sender key.
func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool {
// TODO this may need to be changed if olm sessions start expiring
store.olmSessionCacheLock.Lock()
cache, ok := store.olmSessionCache[key]
store.olmSessionCacheLock.Unlock()
if ok && len(cache) > 0 {
return true
}
var sessionID id.SessionID
err := store.DB.QueryRow("SELECT session_id FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 LIMIT 1",
key, store.AccountID).Scan(&sessionID)
@ -137,53 +147,88 @@ func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool {
// GetSessions returns all the known Olm sessions for a sender key.
func (store *SQLCryptoStore) GetSessions(key id.SenderKey) (OlmSessionList, error) {
rows, err := store.DB.Query("SELECT session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY session_id",
rows, err := store.DB.Query("SELECT session_id, session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY session_id",
key, store.AccountID)
if err != nil {
return nil, err
}
list := OlmSessionList{}
store.olmSessionCacheLock.Lock()
defer store.olmSessionCacheLock.Unlock()
cache := store.getOlmSessionCache(key)
for rows.Next() {
sess := OlmSession{Internal: *olm.NewBlankSession()}
sess := OlmSession{Internal: *olm.NewBlankSession(), lock: &sync.Mutex{}}
var sessionBytes []byte
err := rows.Scan(&sessionBytes, &sess.CreationTime, &sess.UseTime)
var sessionID id.SessionID
err := rows.Scan(&sessionID, &sessionBytes, &sess.CreationTime, &sess.UseTime)
if err != nil {
return nil, err
} else if existing, ok := cache[sessionID]; ok {
list = append(list, existing)
} else {
err = sess.Internal.Unpickle(sessionBytes, store.PickleKey)
if err != nil {
return nil, err
}
list = append(list, &sess)
cache[sess.ID()] = &sess
}
err = sess.Internal.Unpickle(sessionBytes, store.PickleKey)
if err != nil {
return nil, err
}
list = append(list, &sess)
}
return list, nil
}
func (store *SQLCryptoStore) getOlmSessionCache(key id.SenderKey) map[id.SessionID]*OlmSession {
data, ok := store.olmSessionCache[key]
if !ok {
data = make(map[id.SessionID]*OlmSession)
store.olmSessionCache[key] = data
}
return data
}
// GetLatestSession retrieves the Olm session for a given sender key from the database that has the largest ID.
func (store *SQLCryptoStore) GetLatestSession(key id.SenderKey) (*OlmSession, error) {
row := store.DB.QueryRow("SELECT session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY session_id DESC LIMIT 1",
store.olmSessionCacheLock.Lock()
defer store.olmSessionCacheLock.Unlock()
row := store.DB.QueryRow("SELECT session_id, session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY session_id DESC LIMIT 1",
key, store.AccountID)
sess := OlmSession{Internal: *olm.NewBlankSession()}
sess := OlmSession{Internal: *olm.NewBlankSession(), lock: &sync.Mutex{}}
var sessionBytes []byte
err := row.Scan(&sessionBytes, &sess.CreationTime, &sess.UseTime)
var sessionID id.SessionID
err := row.Scan(&sessionID, &sessionBytes, &sess.CreationTime, &sess.UseTime)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
return &sess, sess.Internal.Unpickle(sessionBytes, store.PickleKey)
cache := store.getOlmSessionCache(key)
if oldSess, ok := cache[sessionID]; ok {
return oldSess, nil
} else if err = sess.Internal.Unpickle(sessionBytes, store.PickleKey); err != nil {
return nil, err
} else {
cache[sessionID] = &sess
return &sess, nil
}
}
// AddSession persists an Olm session for a sender in the database.
func (store *SQLCryptoStore) AddSession(key id.SenderKey, session *OlmSession) error {
store.olmSessionCacheLock.Lock()
defer store.olmSessionCacheLock.Unlock()
sessionBytes := session.Internal.Pickle(store.PickleKey)
_, err := store.DB.Exec("INSERT INTO crypto_olm_session (session_id, sender_key, session, created_at, last_used, account_id) VALUES ($1, $2, $3, $4, $5, $6)",
session.ID(), key, sessionBytes, session.CreationTime, session.UseTime, store.AccountID)
store.getOlmSessionCache(key)[session.ID()] = session
return err
}
// UpdateSession replaces the Olm session for a sender in the database.
func (store *SQLCryptoStore) UpdateSession(key id.SenderKey, session *OlmSession) error {
func (store *SQLCryptoStore) UpdateSession(_ id.SenderKey, session *OlmSession) error {
sessionBytes := session.Internal.Pickle(store.PickleKey)
_, err := store.DB.Exec("UPDATE crypto_olm_session SET session=$1, last_used=$2 WHERE session_id=$3 AND account_id=$4",
sessionBytes, session.UseTime, session.ID(), store.AccountID)
@ -224,7 +269,7 @@ func (store *SQLCryptoStore) GetGroupSession(roomID id.RoomID, senderKey id.Send
} else if err != nil {
return nil, err
} else if withheldCode.Valid {
return nil, ErrGroupSessionWithheld
return nil, fmt.Errorf("%w (%s)", ErrGroupSessionWithheld, withheldCode.String)
}
igs := olm.NewBlankInboundGroupSession()
err = igs.Unpickle(sessionBytes, store.PickleKey)
@ -492,18 +537,18 @@ func (store *SQLCryptoStore) PutDevices(userID id.UserID, devices map[id.DeviceI
err = fmt.Errorf("unsupported dialect %s", store.Dialect)
}
if err != nil {
return errors.Wrap(err, "failed to add user to tracked users list")
return fmt.Errorf("failed to add user to tracked users list: %w", err)
}
_, err = tx.Exec("DELETE FROM crypto_device WHERE user_id=$1", userID)
if err != nil {
_ = tx.Rollback()
return errors.Wrap(err, "failed to delete old devices")
return fmt.Errorf("failed to delete old devices: %w", err)
}
if len(devices) == 0 {
err = tx.Commit()
if err != nil {
return errors.Wrap(err, "failed to commit changes (no devices added)")
return fmt.Errorf("failed to commit changes (no devices added): %w", err)
}
return nil
}
@ -533,12 +578,12 @@ func (store *SQLCryptoStore) PutDevices(userID id.UserID, devices map[id.DeviceI
_, err = tx.Exec("INSERT INTO crypto_device (user_id, device_id, identity_key, signing_key, trust, deleted, name) VALUES "+valueString, values...)
if err != nil {
_ = tx.Rollback()
return errors.Wrap(err, "failed to insert new devices")
return fmt.Errorf("failed to insert new devices: %w", err)
}
}
err = tx.Commit()
if err != nil {
return errors.Wrap(err, "failed to commit changes")
return fmt.Errorf("failed to commit changes: %w", err)
}
return nil
}

View file

@ -2,14 +2,15 @@ package sql_store_upgrade
import (
"database/sql"
"errors"
"fmt"
"strings"
"github.com/pkg/errors"
)
type upgradeFunc func(*sql.Tx, string) error
var ErrUnknownDialect = errors.New("unknown dialect")
var Upgrades = [...]upgradeFunc{
func(tx *sql.Tx, _ string) error {
for _, query := range []string{
@ -153,7 +154,7 @@ var Upgrades = [...]upgradeFunc{
}
}
} else {
return errors.New("unknown dialect: " + dialect)
return fmt.Errorf("%w (%s)", ErrUnknownDialect, dialect)
}
return nil
},
@ -203,7 +204,7 @@ var Upgrades = [...]upgradeFunc{
return err
}
} else {
return errors.New("unknown dialect: " + dialect)
return fmt.Errorf("%w (%s)", ErrUnknownDialect, dialect)
}
return nil
},

View file

@ -9,6 +9,7 @@ package crypto
import (
"encoding/gob"
"errors"
"fmt"
"os"
"sort"
"sync"
@ -65,6 +66,7 @@ var ErrGroupSessionWithheld = errors.New("group session has been withheld")
// General implementation details:
// * Get methods should not return errors if the requested data does not exist in the store, they should simply return nil.
// * Update methods may assume that the pointer is the same as what has earlier been added to or fetched from the store.
// * OlmSessions should be cached so that the mutex works. Alternatively, implementations can use OlmSession.SetLock to provide a custom mutex implementation.
type Store interface {
// Flush ensures that everything in the store is persisted to disk.
// This doesn't have to do anything, e.g. for database-backed implementations that persist everything immediately.
@ -311,10 +313,10 @@ func (gs *GobStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, se
gs.lock.Lock()
session, ok := gs.getGroupSessions(roomID, senderKey)[sessionID]
if !ok {
_, ok := gs.getWithheldGroupSessions(roomID, senderKey)[sessionID]
withheld, ok := gs.getWithheldGroupSessions(roomID, senderKey)[sessionID]
gs.lock.Unlock()
if ok {
return nil, ErrGroupSessionWithheld
return nil, fmt.Errorf("%w (%s)", ErrGroupSessionWithheld, withheld.Code)
}
return nil, nil
}

View file

@ -11,6 +11,7 @@ package crypto
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"sort"
@ -19,8 +20,6 @@ import (
"sync"
"time"
"github.com/pkg/errors"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/olm"
@ -28,11 +27,14 @@ import (
"maunium.net/go/mautrix/id"
)
// ErrUnknownTransaction is returned when a key verification message is received with an unknown transaction ID.
var ErrUnknownTransaction = errors.New("Unknown transaction")
// ErrUnknownVerificationMethod is returned when the verification method in a received m.key.verification.start is unknown.
var ErrUnknownVerificationMethod = errors.New("Unknown verification method")
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.
@ -133,7 +135,7 @@ func (mach *OlmMachine) getTransactionState(transactionID string, userID id.User
_ = mach.SendInRoomSASVerificationCancel(verState.inRoomID, userID, transactionID, reason, event.VerificationCancelUserMismatch)
}
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
return nil, errors.New(reason)
return nil, fmt.Errorf("%w %s: %s", ErrUnknownUserForTransaction, transactionID, userID)
}
return verState, nil
}
@ -648,7 +650,7 @@ func (mach *OlmMachine) NewSASVerificationWith(device *DeviceIdentity, hooks Ver
verState.startEventCanonical = string(canonical)
_, loaded := mach.keyVerificationTransactionState.LoadOrStore(device.UserID.String()+":"+transactionID, verState)
if loaded {
return "", errors.New("Transaction already exists")
return "", ErrTransactionAlreadyExists
}
mach.timeoutAfter(verState, transactionID, timeout)

View file

@ -69,7 +69,7 @@ type HTTPError struct {
}
func (e HTTPError) Is(err error) bool {
return errors.Is(e.RespError, err) || errors.Is(e.WrappedError, err)
return (e.RespError != nil && errors.Is(e.RespError, err)) || (e.WrappedError != nil && errors.Is(e.WrappedError, err))
}
func (e HTTPError) IsStatus(code int) bool {

View file

@ -9,8 +9,6 @@ package event
import (
"encoding/json"
"github.com/pkg/errors"
"maunium.net/go/mautrix/id"
)
@ -58,7 +56,7 @@ func (content *EncryptedEventContent) UnmarshalJSON(data []byte) error {
return json.Unmarshal(content.Ciphertext, &content.OlmCiphertext)
case id.AlgorithmMegolmV1:
if len(content.Ciphertext) == 0 || content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' {
return errors.New("input doesn't look like a JSON string")
return id.InputNotJSONString
}
content.MegolmCiphertext = content.Ciphertext[1 : len(content.Ciphertext)-1]
}

View file

@ -32,8 +32,10 @@ func (tc TypeClass) Name() string {
}
const (
// Unknown events
UnknownEventType TypeClass = iota
// Normal message events
MessageEventType TypeClass = iota
MessageEventType
// State events
StateEventType
// Ephemeral events
@ -42,8 +44,6 @@ const (
AccountDataEventType
// Device-to-device events
ToDeviceEventType
// Unknown events
UnknownEventType
)
type Type struct {

5
example/go.mod Normal file
View file

@ -0,0 +1,5 @@
module mautrix-example
go 1.15
require maunium.net/go/mautrix v0.7.6

33
example/go.sum Normal file
View file

@ -0,0 +1,33 @@
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.7.6 h1:jB9oCimPq0mVyolwQBC/9N1fu21AU+Ryq837cLf4gOo=
maunium.net/go/mautrix v0.7.6/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=

64
example/main.go Normal file
View file

@ -0,0 +1,64 @@
// Copyright (C) 2017 Tulir Asokan
// Copyright (C) 2018-2020 Luca Weiss
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"flag"
"fmt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"os"
)
var homeserver = flag.String("homeserver", "", "Matrix homeserver")
var username = flag.String("username", "", "Matrix username localpart")
var password = flag.String("password", "", "Matrix password")
func main() {
flag.Parse()
if *username == "" || *password == "" || *homeserver == "" {
_, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
os.Exit(1)
}
fmt.Println("Logging into", *homeserver, "as", *username)
client, err := mautrix.NewClient(*homeserver, "", "")
if err != nil {
panic(err)
}
_, err = client.Login(&mautrix.ReqLogin{
Type: "m.login.password",
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: *username},
Password: *password,
StoreCredentials: true,
})
if err != nil {
panic(err)
}
fmt.Println("Login successful")
syncer := client.Syncer.(*mautrix.DefaultSyncer)
syncer.OnEventType(event.EventMessage, func(source mautrix.EventSource, evt *event.Event) {
fmt.Printf("<%[1]s> %[4]s (%[2]s/%[3]s)\n", evt.Sender, evt.Type.String(), evt.ID, evt.Content.AsMessage().Body)
})
err = client.Sync()
if err != nil {
panic(err)
}
}

5
go.mod
View file

@ -7,14 +7,13 @@ require (
github.com/gorilla/mux v1.7.4
github.com/lib/pq v1.7.0
github.com/mattn/go-sqlite3 v1.14.0
github.com/pkg/errors v0.9.1
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/stretchr/testify v1.6.1
github.com/tidwall/gjson v1.6.0
github.com/tidwall/sjson v1.1.1
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
golang.org/x/net v0.0.0-20201026091529-146b70c837a4
gopkg.in/yaml.v2 v2.3.0
maunium.net/go/maulogger/v2 v2.1.1
)

29
go.sum
View file

@ -12,68 +12,55 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8=
github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U=
github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs=
golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA=
maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=

View file

@ -19,6 +19,8 @@ const (
AuthTypeMSISDN = "m.login.msisdn"
AuthTypeToken = "m.login.token"
AuthTypeDummy = "m.login.dummy"
AuthTypeAppservice = "uk.half-shot.msc2778.login.application_service"
)
type IdentifierType string

View file

@ -119,10 +119,19 @@ type RespRegister struct {
type RespLoginFlows struct {
Flows []struct {
Type string `json:"type"`
Type AuthType `json:"type"`
} `json:"flows"`
}
func (rlf *RespLoginFlows) HasFlow(flowType AuthType) bool {
for _, flow := range rlf.Flows {
if flow.Type == flowType {
return true
}
}
return false
}
// RespLogin is the JSON response for http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
type RespLogin struct {
AccessToken string `json:"access_token"`

View file

@ -1,3 +1,3 @@
package mautrix
const Version = "v0.7.6"
const Version = "v0.7.13"