mirror of https://github.com/matrix-org/go-neb.git
Browse Source
Enable e2ee across all services and save crypto material in the database (#324)
Enable e2ee across all services and save crypto material in the database (#324)
* Add device ID to the configuration Signed-off-by: Nikos Filippakis <me@nfil.dev> * Basic e2ee support for some commands Signed-off-by: Nikos Filippakis <me@nfil.dev> * Move some of the client and crypto logic to a new BotClient type Signed-off-by: Nikos Filippakis <me@nfil.dev> * Use the state store to retrieve room joined users Signed-off-by: Nikos Filippakis <me@nfil.dev> * Start creating the database APIs for the crypto store Signed-off-by: Nikos Filippakis <me@nfil.dev> * Replace mautrix.Client usage with BotClient for all services to use the e2ee-enabled client Signed-off-by: Nikos Filippakis <me@nfil.dev> * Use SQL backend for storing crypto material Signed-off-by: Nikos Filippakis <me@nfil.dev> * Perform a sync request with full state when starting Signed-off-by: Nikos Filippakis <me@nfil.dev> * Consider case where device ID is empty and log a warning Signed-off-by: Nikos Filippakis <me@nfil.dev>pull/327/head
Nikos Filippakis
5 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 434 additions and 134 deletions
-
2api/api.go
-
3api/handlers/service.go
-
143clients/bot_client.go
-
146clients/clients.go
-
5clients/clients_test.go
-
28clients/crypto_logger.go
-
79clients/state_store.go
-
8database/db.go
-
9go.mod
-
17go.sum
-
2goneb.go
-
8services/alertmanager/alertmanager.go
-
2services/alertmanager/alertmanager_test.go
-
5services/echo/echo.go
-
5services/giphy/giphy.go
-
7services/github/github.go
-
7services/github/github_webhook.go
-
6services/google/google.go
-
5services/guggy/guggy.go
-
5services/imgur/imgur.go
-
11services/jira/jira.go
-
10services/rssbot/rssbot.go
-
6services/slackapi/slackapi.go
-
8services/travisci/travisci.go
-
5services/wikipedia/wikipedia.go
-
30types/service.go
@ -0,0 +1,143 @@ |
|||
package clients |
|||
|
|||
import ( |
|||
"time" |
|||
|
|||
"github.com/matrix-org/go-neb/api" |
|||
"github.com/matrix-org/go-neb/database" |
|||
"github.com/matrix-org/go-neb/matrix" |
|||
log "github.com/sirupsen/logrus" |
|||
"maunium.net/go/mautrix" |
|||
"maunium.net/go/mautrix/crypto" |
|||
mevt "maunium.net/go/mautrix/event" |
|||
"maunium.net/go/mautrix/id" |
|||
) |
|||
|
|||
// BotClient represents one of the bot's sessions, with a specific User and Device ID.
|
|||
// It can be used for sending messages and retrieving information about the rooms that
|
|||
// the client has joined.
|
|||
type BotClient struct { |
|||
*mautrix.Client |
|||
config api.ClientConfig |
|||
olmMachine *crypto.OlmMachine |
|||
stateStore *NebStateStore |
|||
} |
|||
|
|||
// InitOlmMachine initializes a BotClient's internal OlmMachine given a client object and a Neb store,
|
|||
// which will be used to store room information.
|
|||
func (botClient *BotClient) InitOlmMachine(client *mautrix.Client, nebStore *matrix.NEBStore) (err error) { |
|||
|
|||
var cryptoStore crypto.Store |
|||
cryptoLogger := CryptoMachineLogger{} |
|||
if sdb, ok := database.GetServiceDB().(*database.ServiceDB); ok { |
|||
// Create an SQL crypto store based on the ServiceDB used
|
|||
db, dialect := sdb.GetSQLDb() |
|||
sqlCryptoStore := crypto.NewSQLCryptoStore(db, dialect, client.DeviceID, []byte(client.DeviceID.String()), cryptoLogger) |
|||
// Try to create the tables if they are missing
|
|||
if err = sqlCryptoStore.CreateTables(); err != nil { |
|||
return |
|||
} |
|||
cryptoStore = sqlCryptoStore |
|||
cryptoLogger.Debug("Using SQL backend as the crypto store") |
|||
} else { |
|||
deviceID := client.DeviceID.String() |
|||
if deviceID == "" { |
|||
deviceID = "_empty_device_id" |
|||
} |
|||
cryptoStore, err = crypto.NewGobStore(deviceID + ".gob") |
|||
if err != nil { |
|||
return |
|||
} |
|||
cryptoLogger.Debug("Using gob storage as the crypto store") |
|||
} |
|||
|
|||
botClient.stateStore = &NebStateStore{&nebStore.InMemoryStore} |
|||
olmMachine := crypto.NewOlmMachine(client, cryptoLogger, cryptoStore, botClient.stateStore) |
|||
if err = olmMachine.Load(); err != nil { |
|||
return |
|||
} |
|||
botClient.olmMachine = olmMachine |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Register registers a BotClient's Sync and StateMember event callbacks to update its internal state
|
|||
// when new events arrive.
|
|||
func (botClient *BotClient) Register(syncer mautrix.ExtensibleSyncer) { |
|||
syncer.OnEventType(mevt.StateMember, func(_ mautrix.EventSource, evt *mevt.Event) { |
|||
botClient.olmMachine.HandleMemberEvent(evt) |
|||
}) |
|||
syncer.OnSync(botClient.syncCallback) |
|||
} |
|||
|
|||
func (botClient *BotClient) syncCallback(resp *mautrix.RespSync, since string) bool { |
|||
botClient.stateStore.UpdateStateStore(resp) |
|||
botClient.olmMachine.ProcessSyncResponse(resp, since) |
|||
if err := botClient.olmMachine.CryptoStore.Flush(); err != nil { |
|||
log.WithError(err).Error("Could not flush crypto store") |
|||
} |
|||
return true |
|||
} |
|||
|
|||
// DecryptMegolmEvent attempts to decrypt an incoming m.room.encrypted message using the session information
|
|||
// already present in the OlmMachine. The corresponding decrypted event is then returned.
|
|||
// If it fails, usually because the session is not known, an error is returned.
|
|||
func (botClient *BotClient) DecryptMegolmEvent(evt *mevt.Event) (*mevt.Event, error) { |
|||
return botClient.olmMachine.DecryptMegolmEvent(evt) |
|||
} |
|||
|
|||
// SendMessageEvent sends the given content to the given room ID using this BotClient as a message event.
|
|||
// If the target room has enabled encryption, a megolm session is created if one doesn't already exist
|
|||
// and the message is sent after being encrypted.
|
|||
func (botClient *BotClient) SendMessageEvent(roomID id.RoomID, evtType mevt.Type, content interface{}, |
|||
extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) { |
|||
|
|||
olmMachine := botClient.olmMachine |
|||
if olmMachine.StateStore.IsEncrypted(roomID) { |
|||
// Check if there is already a megolm session
|
|||
if sess, err := olmMachine.CryptoStore.GetOutboundGroupSession(roomID); err != nil { |
|||
return nil, err |
|||
} else if sess == nil || sess.Expired() || !sess.Shared { |
|||
// No error but valid, shared session does not exist
|
|||
memberIDs, err := botClient.stateStore.GetJoinedMembers(roomID) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
// Share group session with room members
|
|||
if err = olmMachine.ShareGroupSession(roomID, memberIDs); err != nil { |
|||
return nil, err |
|||
} |
|||
} |
|||
enc, err := olmMachine.EncryptMegolmEvent(roomID, mevt.EventMessage, content) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
content = enc |
|||
evtType = mevt.EventEncrypted |
|||
} |
|||
return botClient.Client.SendMessageEvent(roomID, evtType, content, extra...) |
|||
} |
|||
|
|||
// Sync loops to keep syncing the client with the homeserver by calling the /sync endpoint.
|
|||
func (botClient *BotClient) Sync() { |
|||
// Get the state store up to date
|
|||
resp, err := botClient.SyncRequest(30000, "", "", true, mevt.PresenceOnline) |
|||
if err != nil { |
|||
log.WithError(err).Error("Error performing initial sync") |
|||
return |
|||
} |
|||
botClient.stateStore.UpdateStateStore(resp) |
|||
|
|||
for { |
|||
if e := botClient.Client.Sync(); e != nil { |
|||
log.WithFields(log.Fields{ |
|||
log.ErrorKey: e, |
|||
"user_id": botClient.config.UserID, |
|||
}).Error("Fatal Sync() error") |
|||
time.Sleep(10 * time.Second) |
|||
} else { |
|||
log.WithField("user_id", botClient.config.UserID).Info("Stopping Sync()") |
|||
return |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
package clients |
|||
|
|||
import ( |
|||
log "github.com/sirupsen/logrus" |
|||
) |
|||
|
|||
// CryptoMachineLogger wraps around the usual logger, implementing the Logger interface needed by OlmMachine.
|
|||
type CryptoMachineLogger struct{} |
|||
|
|||
// Error formats and logs an error message.
|
|||
func (CryptoMachineLogger) Error(message string, args ...interface{}) { |
|||
log.Errorf(message, args...) |
|||
} |
|||
|
|||
// Warn formats and logs a warning message.
|
|||
func (CryptoMachineLogger) Warn(message string, args ...interface{}) { |
|||
log.Warnf(message, args...) |
|||
} |
|||
|
|||
// Debug formats and logs a debug message.
|
|||
func (CryptoMachineLogger) Debug(message string, args ...interface{}) { |
|||
log.Debugf(message, args...) |
|||
} |
|||
|
|||
// Trace formats and logs a trace message.
|
|||
func (CryptoMachineLogger) Trace(message string, args ...interface{}) { |
|||
log.Tracef(message, args...) |
|||
} |
@ -0,0 +1,79 @@ |
|||
package clients |
|||
|
|||
import ( |
|||
"errors" |
|||
|
|||
"maunium.net/go/mautrix" |
|||
"maunium.net/go/mautrix/event" |
|||
"maunium.net/go/mautrix/id" |
|||
) |
|||
|
|||
// NebStateStore implements the StateStore interface for OlmMachine.
|
|||
// It is used to determine which rooms are encrypted and which rooms are shared with a user.
|
|||
// The state is updated by /sync responses.
|
|||
type NebStateStore struct { |
|||
Storer *mautrix.InMemoryStore |
|||
} |
|||
|
|||
// IsEncrypted returns whether a room has been encrypted.
|
|||
func (ss *NebStateStore) IsEncrypted(roomID id.RoomID) bool { |
|||
room := ss.Storer.LoadRoom(roomID) |
|||
if room == nil { |
|||
return false |
|||
} |
|||
_, ok := room.State[event.StateEncryption] |
|||
return ok |
|||
} |
|||
|
|||
// FindSharedRooms returns a list of room IDs that the given user ID is also a member of.
|
|||
func (ss *NebStateStore) FindSharedRooms(userID id.UserID) []id.RoomID { |
|||
sharedRooms := make([]id.RoomID, 0) |
|||
for roomID, room := range ss.Storer.Rooms { |
|||
if room.GetMembershipState(userID) != event.MembershipLeave { |
|||
sharedRooms = append(sharedRooms, roomID) |
|||
} |
|||
} |
|||
return sharedRooms |
|||
} |
|||
|
|||
// UpdateStateStore updates the internal state of NebStateStore from a /sync response.
|
|||
func (ss *NebStateStore) UpdateStateStore(resp *mautrix.RespSync) { |
|||
for roomID, evts := range resp.Rooms.Join { |
|||
room := ss.Storer.LoadRoom(roomID) |
|||
if room == nil { |
|||
room = mautrix.NewRoom(roomID) |
|||
ss.Storer.SaveRoom(room) |
|||
} |
|||
for _, i := range evts.State.Events { |
|||
room.UpdateState(i) |
|||
} |
|||
for _, i := range evts.Timeline.Events { |
|||
if i.Type.IsState() { |
|||
room.UpdateState(i) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// GetJoinedMembers returns a list of members that are currently in a room.
|
|||
func (ss *NebStateStore) GetJoinedMembers(roomID id.RoomID) ([]id.UserID, error) { |
|||
joinedMembers := make([]id.UserID, 0) |
|||
room := ss.Storer.LoadRoom(roomID) |
|||
if room == nil { |
|||
return nil, errors.New("unknown roomID") |
|||
} |
|||
memberEvents := room.State[event.StateMember] |
|||
if memberEvents == nil { |
|||
return nil, errors.New("no state member events found") |
|||
} |
|||
for stateKey, stateEvent := range memberEvents { |
|||
if stateEvent == nil { |
|||
continue |
|||
} |
|||
stateEvent.Content.ParseRaw(event.StateMember) |
|||
if stateEvent.Content.AsMember().Membership == event.MembershipJoin { |
|||
joinedMembers = append(joinedMembers, id.UserID(stateKey)) |
|||
} |
|||
} |
|||
return joinedMembers, nil |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue