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