Browse Source

SAS verification and cryptotest service (#333)

* Handle SAS verification by exposing an endpoint where the SAS can be sent to be verified

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Init cryptotest service with basic commands

* Add cryptotest service with challenge request / response / session invalidation commands

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Add cryptotest methods for testing key forwarding

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Add help messages for cryptotest cmds

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Add newer apt repository for updated libolm

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Move command functionalities for cryptotest to different functions

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Fixed cryptotest service and package name from echo

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Control which users can start a SAS verification with Neb through regexes in the config

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Have maximum number of ongoing verifications at any time

Signed-off-by: Nikos Filippakis <me@nfil.dev>

* Fix Trace to Tracef

Signed-off-by: Nikos Filippakis <me@nfil.dev>
pull/337/head
Nikos Filippakis 4 years ago
committed by GitHub
parent
commit
b6d70d04b6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .travis.yml
  2. 19
      README.md
  3. 24
      api/api.go
  4. 53
      api/handlers/client.go
  5. 180
      clients/bot_client.go
  6. 4
      clients/clients.go
  7. 48
      clients/clients_test.go
  8. 2
      config.sample.yaml
  9. 2
      go.mod
  10. 2
      go.sum
  11. 3
      goneb.go
  12. 353
      services/cryptotest/cryptotest.go

3
.travis.yml

@ -4,8 +4,9 @@ language: go
go:
- 1.14
install:
- echo 'deb http://archive.ubuntu.com/ubuntu/ focal universe' | sudo tee -a /etc/apt/sources.list
- sudo apt-get update
- sudo apt-get -y install libolm2 libolm-dev
- sudo apt-get -y install libolm3 libolm-dev
- go get golang.org/x/lint/golint
- go get github.com/fzipp/gocyclo

19
README.md

@ -13,6 +13,7 @@ Go-NEB is a [Matrix](https://matrix.org) bot written in Go. It is the successor
* [Configuring clients](#configuring-clients)
* [Configuring services](#configuring-services)
* [Configuring realms](#configuring-realms)
* [SAS verification](#sas-verification)
* [Developing](#developing)
* [Architecture](#architecture)
* [API Docs](#viewing-the-api-docs)
@ -178,6 +179,22 @@ Authentication via the config file:
- [Github](https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/realms/github/index.html#Session)
- [JIRA](https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/realms/jira/index.html#Session)
## SAS verification
Go-NEB supports SAS verification using the decimal method. Another user can start a verification transaction with Go-NEB using their client, and it will be accepted. In order to confirm the devices, the 3 SAS integers must then be sent to Go-NEB, to the endpoint '/verifySAS' so that it can mark the device as trusted.
For example, if your user ID is `@user:localhost` and your device ID is `ABCD`, you start a SAS verification with Go-NEB and get the SAS "1111 2222 3333". You can perform the following curl request to let Go-NEB know the SAS integers so that it can match them with its own:
```bash
curl -X POST --header 'Content-Type: application/json' -d '{
"UserID": "@neb:localhost",
"OtherUserID": "@user:localhost",
"OtherDeviceID": "ABCD",
"SAS": [1111,2222,3333]
}' 'http://localhost:4050/verifySAS'
```
If the SAS match and you also confirm that via the other device's client, the verification should finish successfully.
# Contributing
Before submitting pull requests, please read the [Matrix.org contribution guidelines](https://github.com/matrix-org/synapse/blob/develop/CONTRIBUTING.md#sign-off) regarding sign-off of your work.
@ -185,7 +202,7 @@ Before submitting pull requests, please read the [Matrix.org contribution guidel
# Developing
This project depends on `libolm` for the end-to-end encryption. Therefore,
you need to install `libolm2` and `libolm-dev` on Ubuntu / `libolm-devel` on
you need to install `libolm3` and `libolm-dev` on Ubuntu / `libolm-devel` on
CentOS to be able to build and run it.
There's a bunch more tools this project uses when developing in order to do

24
api/api.go

@ -78,6 +78,22 @@ type ClientConfig struct {
// The desired display name for this client.
// This does not automatically set the display name for this client. See /configureClient.
DisplayName string
// A list of regexes that control which users are allowed to start a SAS verification with this client.
// When a user starts a new SAS verification with us, their user ID has to match one of these regexes
// for the verification process to start.
AcceptVerificationFromUsers []string
}
// A IncomingDecimalSAS contains the decimal SAS as displayed on another device. The SAS consists of three numbers.
type IncomingDecimalSAS struct {
// The matrix User ID of the user that Neb uses in the verification process. E.g. @neb:localhost
UserID id.UserID
// The three numbers that the SAS consists of.
SAS [3]uint
// The matrix User ID of the other user whose device is being verified.
OtherUserID id.UserID
// The matrix Device ID of the other device that is being verified.
OtherDeviceID id.DeviceID
}
// Session contains the complete auth session information for a given user on a given realm.
@ -132,6 +148,14 @@ func (c *ClientConfig) Check() error {
return nil
}
// Check that the received SAS data contains the correct fields.
func (c *IncomingDecimalSAS) Check() error {
if c.UserID == "" || c.OtherUserID == "" || c.OtherDeviceID == "" {
return errors.New(`Must supply a "UserID", an "OtherUserID", and an "OtherDeviceID"`)
}
return nil
}
// Check that the request is valid.
func (r *RequestAuthSessionRequest) Check() error {
if r.UserID == "" || r.RealmID == "" || r.Config == nil {

53
api/handlers/client.go

@ -7,6 +7,7 @@ import (
"github.com/matrix-org/go-neb/api"
"github.com/matrix-org/go-neb/clients"
"github.com/matrix-org/util"
"maunium.net/go/mautrix/crypto"
)
// ConfigureClient represents an HTTP handler capable of processing /admin/configureClient requests.
@ -67,3 +68,55 @@ func (s *ConfigureClient) OnIncomingRequest(req *http.Request) util.JSONResponse
}{oldClient, body},
}
}
// VerifySAS represents an HTTP handler capable of processing /verifySAS requests.
type VerifySAS struct {
Clients *clients.Clients
}
// OnIncomingRequest handles POST requests to /verifySAS. The JSON object provided
// is of type "api.IncomingDecimalSAS".
//
// The request should contain the three decimal SAS numbers as displayed on the other device that is being verified,
// as well as that device's user and device ID.
// It should also contain the user ID that Go-NEB's client is using.
//
// Request:
// POST /verifySAS
// {
// "UserID": "@my_bot:localhost", // Neb's user ID
// "OtherUserID": "@user:localhost", // User ID of device we're verifying with
// "OtherDeviceID": "ABCDEFG", // Device ID of device we're verifying with
// "SAS": [1111, 2222, 3333] // SAS displayed on device we're verifying with
// }
//
// Response:
// HTTP/1.1 200 OK
// {}
func (s *VerifySAS) OnIncomingRequest(req *http.Request) util.JSONResponse {
if req.Method != "POST" {
return util.MessageResponse(405, "Unsupported Method")
}
var body api.IncomingDecimalSAS
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return util.MessageResponse(400, "Error parsing request JSON: "+err.Error())
}
if err := body.Check(); err != nil {
return util.MessageResponse(400, "Request error: "+err.Error())
}
client, err := s.Clients.Client(body.UserID)
if err != nil {
util.GetLogger(req.Context()).WithError(err).WithField("body", body).Error("Failed to load client")
return util.MessageResponse(500, "Error storing SAS")
}
client.SubmitDecimalSAS(body.OtherUserID, body.OtherDeviceID, crypto.DecimalSASData(body.SAS))
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}

180
clients/bot_client.go

@ -1,26 +1,38 @@
package clients
import (
"errors"
"regexp"
"sync"
"sync/atomic"
"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"
"golang.org/x/net/context"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/event"
mevt "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// maximumVerifications is the number of maximum ongoing SAS verifications at a time.
// After this limit we start ignoring verification requests.
const maximumVerifications = 100
// 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
config api.ClientConfig
olmMachine *crypto.OlmMachine
stateStore *NebStateStore
verificationSAS *sync.Map
ongoingVerificationCount int32
}
// InitOlmMachine initializes a BotClient's internal OlmMachine given a client object and a Neb store,
@ -54,6 +66,31 @@ func (botClient *BotClient) InitOlmMachine(client *mautrix.Client, nebStore *mat
botClient.stateStore = &NebStateStore{&nebStore.InMemoryStore}
olmMachine := crypto.NewOlmMachine(client, cryptoLogger, cryptoStore, botClient.stateStore)
regexes := make([]*regexp.Regexp, 0, len(botClient.config.AcceptVerificationFromUsers))
for _, userRegex := range botClient.config.AcceptVerificationFromUsers {
regex, err := regexp.Compile(userRegex)
if err != nil {
cryptoLogger.Error("Error compiling regex %v: %v", userRegex, err)
} else {
regexes = append(regexes, regex)
}
}
olmMachine.AcceptVerificationFrom = func(_ string, otherDevice *crypto.DeviceIdentity) (crypto.VerificationRequestResponse, crypto.VerificationHooks) {
for _, regex := range regexes {
if regex.MatchString(otherDevice.UserID.String()) {
if atomic.LoadInt32(&botClient.ongoingVerificationCount) >= maximumVerifications {
cryptoLogger.Trace("User ID %v matches regex %v but we are currently at maximum verifications, ignoring...", otherDevice.UserID, regex)
return crypto.IgnoreRequest, botClient
}
cryptoLogger.Trace("User ID %v matches regex %v, accepting SAS request", otherDevice.UserID, regex)
atomic.AddInt32(&botClient.ongoingVerificationCount, 1)
return crypto.AcceptRequest, botClient
}
}
cryptoLogger.Trace("User ID %v does not match any regex, rejecting SAS request", otherDevice.UserID)
return crypto.RejectRequest, botClient
}
if err = olmMachine.Load(); err != nil {
return
}
@ -142,3 +179,140 @@ func (botClient *BotClient) Sync() {
}
}
}
// VerifySASMatch returns whether the received SAS matches the SAS that the bot generated.
// It retrieves the SAS of the other device from the bot client's SAS sync map, where it was stored by the `SubmitDecimalSAS` function.
func (botClient *BotClient) VerifySASMatch(otherDevice *crypto.DeviceIdentity, sas crypto.SASData) bool {
log.WithFields(log.Fields{
"otherUser": otherDevice.UserID,
"otherDevice": otherDevice.DeviceID,
}).Infof("Waiting for SAS")
if sas.Type() != event.SASDecimal {
log.Warnf("Unsupported SAS type: %v", sas.Type())
return false
}
key := otherDevice.UserID.String() + ":" + otherDevice.DeviceID.String()
sasChan, loaded := botClient.verificationSAS.LoadOrStore(key, make(chan crypto.DecimalSASData))
if !loaded {
// if we created the chan, delete it after the timeout duration
defer botClient.verificationSAS.Delete(key)
}
select {
case otherSAS := <-sasChan.(chan crypto.DecimalSASData):
ourSAS := sas.(crypto.DecimalSASData)
log.WithFields(log.Fields{
"otherUser": otherDevice.UserID,
"otherDevice": otherDevice.DeviceID,
}).Warnf("Our SAS: %v, Received SAS: %v, Match: %v", ourSAS, otherSAS, ourSAS == otherSAS)
return ourSAS == otherSAS
case <-time.After(botClient.olmMachine.DefaultSASTimeout):
log.Warnf("Timed out while waiting for SAS from device %v", otherDevice.DeviceID)
}
return false
}
// SubmitDecimalSAS stores the received decimal SAS from another device to compare to the local one.
// It stores the SAS in the bot client's SAS sync map to be retrieved from the `VerifySASMatch` function.
func (botClient *BotClient) SubmitDecimalSAS(otherUser id.UserID, otherDevice id.DeviceID, sas crypto.DecimalSASData) {
key := otherUser.String() + ":" + otherDevice.String()
sasChan, loaded := botClient.verificationSAS.LoadOrStore(key, make(chan crypto.DecimalSASData))
go func() {
if !loaded {
// if we created the chan, delete it after the timeout duration
defer botClient.verificationSAS.Delete(key)
}
// insert to channel in goroutine to avoid blocking if we are not expecting a SAS for this user/device right now
select {
case sasChan.(chan crypto.DecimalSASData) <- crypto.DecimalSASData(sas):
case <-time.After(botClient.olmMachine.DefaultSASTimeout):
log.Warnf("Timed out while trying to send SAS for device %v", otherDevice)
}
}()
}
// VerificationMethods returns the supported SAS verification methods.
// As a bot we only support decimal as it's easier to understand.
func (botClient *BotClient) VerificationMethods() []crypto.VerificationMethod {
return []crypto.VerificationMethod{
crypto.VerificationMethodDecimal{},
}
}
// OnCancel is called when a SAS verification is canceled.
func (botClient *BotClient) OnCancel(cancelledByUs bool, reason string, reasonCode event.VerificationCancelCode) {
atomic.AddInt32(&botClient.ongoingVerificationCount, -1)
log.Tracef("Verification cancelled with reason: %v", reason)
}
// OnSuccess is called when a SAS verification is successful.
func (botClient *BotClient) OnSuccess() {
atomic.AddInt32(&botClient.ongoingVerificationCount, -1)
log.Trace("Verification was successful")
}
// InvalidateRoomSession invalidates the outbound group session for the given room.
func (botClient *BotClient) InvalidateRoomSession(roomID id.RoomID) (id.SessionID, error) {
outbound, err := botClient.olmMachine.CryptoStore.GetOutboundGroupSession(roomID)
if err != nil {
return "", err
}
if outbound == nil {
return "", errors.New("No group session found for this room")
}
return outbound.ID(), botClient.olmMachine.CryptoStore.RemoveOutboundGroupSession(roomID)
}
// StartSASVerification starts a new SAS verification with the given user and device ID and returns the transaction ID if successful.
func (botClient *BotClient) StartSASVerification(userID id.UserID, deviceID id.DeviceID) (string, error) {
device, err := botClient.olmMachine.GetOrFetchDevice(userID, deviceID)
if err != nil {
return "", err
}
return botClient.olmMachine.NewSimpleSASVerificationWith(device, botClient)
}
// SendRoomKeyRequest sends a room key request to another device.
func (botClient *BotClient) SendRoomKeyRequest(userID id.UserID, deviceID id.DeviceID, roomID id.RoomID,
senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) (chan bool, error) {
ctx, _ := context.WithTimeout(context.Background(), timeout)
return botClient.olmMachine.RequestRoomKey(ctx, userID, deviceID, roomID, senderKey, sessionID)
}
// ForwardRoomKeyToDevice sends a room key to another device.
func (botClient *BotClient) ForwardRoomKeyToDevice(userID id.UserID, deviceID id.DeviceID, roomID id.RoomID, senderKey id.SenderKey,
sessionID id.SessionID) error {
device, err := botClient.olmMachine.GetOrFetchDevice(userID, deviceID)
if err != nil {
return err
}
igs, err := botClient.olmMachine.CryptoStore.GetGroupSession(roomID, senderKey, sessionID)
if err != nil {
return err
} else if igs == nil {
return errors.New("Group session not found")
}
exportedKey, err := igs.Internal.Export(igs.Internal.FirstKnownIndex())
if err != nil {
return err
}
forwardedRoomKey := event.Content{
Parsed: &event.ForwardedRoomKeyEventContent{
RoomKeyEventContent: event.RoomKeyEventContent{
Algorithm: id.AlgorithmMegolmV1,
RoomID: igs.RoomID,
SessionID: igs.ID(),
SessionKey: exportedKey,
},
SenderKey: senderKey,
ForwardingKeyChain: igs.ForwardingChains,
SenderClaimedKey: igs.SigningKey,
},
}
return botClient.olmMachine.SendEncryptedToDevice(device, forwardedRoomKey)
}

4
clients/clients.go

@ -4,6 +4,7 @@ import (
"database/sql"
"fmt"
"net/http"
"reflect"
"strings"
"sync"
@ -111,7 +112,7 @@ func (c *Clients) updateClientInDB(newConfig api.ClientConfig) (new, old BotClie
defer c.dbMutex.Unlock()
old = c.getClient(newConfig.UserID)
if old.Client != nil && old.config == newConfig {
if old.Client != nil && reflect.DeepEqual(old.config, newConfig) {
// Already have a client with that config.
new = old
return
@ -340,6 +341,7 @@ func (c *Clients) initClient(botClient *BotClient) error {
log.Warn("Device ID is not set which will result in E2E encryption/decryption not working")
}
botClient.Client = client
botClient.verificationSAS = &sync.Map{}
syncer := client.Syncer.(*mautrix.DefaultSyncer)

48
clients/clients_test.go

@ -4,11 +4,14 @@ import (
"fmt"
"net/http"
"reflect"
"sync"
"testing"
"time"
"github.com/matrix-org/go-neb/database"
"github.com/matrix-org/go-neb/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
mevt "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@ -103,3 +106,48 @@ func TestCommandParsing(t *testing.T) {
}
}
func TestSASVerificationHandling(t *testing.T) {
botClient := BotClient{verificationSAS: &sync.Map{}}
botClient.olmMachine = &crypto.OlmMachine{
DefaultSASTimeout: time.Minute,
}
otherUserID := id.UserID("otherUser")
otherDeviceID := id.DeviceID("otherDevice")
otherDevice := &crypto.DeviceIdentity{
UserID: otherUserID,
DeviceID: otherDeviceID,
}
botClient.SubmitDecimalSAS(otherUserID, otherDeviceID, crypto.DecimalSASData([3]uint{4, 5, 6}))
matched := botClient.VerifySASMatch(otherDevice, crypto.DecimalSASData([3]uint{1, 2, 3}))
if matched {
t.Error("SAS matched when they shouldn't have")
}
botClient.SubmitDecimalSAS(otherUserID, otherDeviceID, crypto.DecimalSASData([3]uint{1, 2, 3}))
matched = botClient.VerifySASMatch(otherDevice, crypto.DecimalSASData([3]uint{1, 2, 3}))
if !matched {
t.Error("Expected SAS to match but they didn't")
}
botClient.SubmitDecimalSAS(otherUserID+"wrong", otherDeviceID, crypto.DecimalSASData([3]uint{4, 5, 6}))
finished := make(chan bool)
go func() {
matched := botClient.VerifySASMatch(otherDevice, crypto.DecimalSASData([3]uint{1, 2, 3}))
finished <- true
if !matched {
t.Error("SAS didn't match when it should have (receiving SAS after calling verification func)")
}
}()
select {
case <-finished:
t.Error("Verification finished before receiving the SAS from the correct user")
default:
}
botClient.SubmitDecimalSAS(otherUserID, otherDeviceID, crypto.DecimalSASData([3]uint{1, 2, 3}))
select {
case <-finished:
case <-time.After(10 * time.Second):
t.Error("Verification did not finish after receiving the SAS from the correct user")
}
}

2
config.sample.yaml

@ -26,6 +26,7 @@ clients:
Sync: true
AutoJoinRooms: true
DisplayName: "Go-NEB!"
AcceptVerificationFromUsers: [":localhost:8008"]
- UserID: "@another_goneb:localhost"
AccessToken: "MDASDASJDIASDJASDAFGFRGER"
@ -34,6 +35,7 @@ clients:
Sync: false
AutoJoinRooms: false
DisplayName: "Go-NEB!"
AcceptVerificationFromUsers: ["^@admin:localhost:8008$"]
# The list of realms which Go-NEB is aware of.
# Delete or modify this list as appropriate.

2
go.mod

@ -51,5 +51,5 @@ require (
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.3.0
maunium.net/go/mautrix v0.6.0
maunium.net/go/mautrix v0.7.0
)

2
go.sum

@ -272,5 +272,7 @@ maunium.net/go/mautrix v0.5.5 h1:e0Pql1FdxoNUudx2oXo1gZHMrqIh5MC72cdXEPIrYLA=
maunium.net/go/mautrix v0.5.5/go.mod h1:FLbMANzwqlsX2Fgm7SDe+E4I3wSa4UxJRKqS5wGkCwA=
maunium.net/go/mautrix v0.6.0 h1:V32l4aygKk2XcH3fi8Yd0pFeSyYZJNRIvr8vdA2GtC8=
maunium.net/go/mautrix v0.6.0/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
maunium.net/go/mautrix v0.7.0 h1:9Wxs5S4Wl4S99dbBwfLZYAe/sP7VKaFikw9Ocf88kfk=
maunium.net/go/mautrix v0.7.0/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
maunium.net/go/mauview v0.1.1/go.mod h1:3QBUiuLct9moP1LgDhCGIg0Ovxn38Bd2sGndnUOuj4o=
maunium.net/go/tcell v0.2.0/go.mod h1:9Apcb3lNNS6C6lCqKT9UFp7BTRzHXfWE+/tgufsAMho=

3
goneb.go

@ -21,6 +21,7 @@ import (
_ "github.com/matrix-org/go-neb/realms/jira"
_ "github.com/matrix-org/go-neb/services/alertmanager"
_ "github.com/matrix-org/go-neb/services/cryptotest"
_ "github.com/matrix-org/go-neb/services/echo"
_ "github.com/matrix-org/go-neb/services/giphy"
_ "github.com/matrix-org/go-neb/services/github"
@ -189,6 +190,8 @@ func setup(e envVars, mux *http.ServeMux, matrixClient *http.Client) {
rh := &handlers.RealmRedirect{db}
mux.HandleFunc("/realms/redirects/", prometheus.InstrumentHandlerFunc("realmRedirectHandler", util.Protect(rh.Handle)))
mux.Handle("/verifySAS", prometheus.InstrumentHandler("verifySAS", util.MakeJSONAPI(&handlers.VerifySAS{matrixClients})))
// Read exclusively from the config file if one was supplied.
// Otherwise, add HTTP listeners for new Services/Sessions/Clients/etc.
if e.ConfigFile != "" {

353
services/cryptotest/cryptotest.go

@ -0,0 +1,353 @@
// Package cryptotest implements a Service which provides several commands for testing the e2e functionalities of other devices.
package cryptotest
import (
"fmt"
"math/rand"
"strconv"
"time"
"github.com/matrix-org/go-neb/clients"
"github.com/matrix-org/go-neb/types"
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"
)
// ServiceType of the Cryptotest service
const ServiceType = "cryptotest"
var expectedString map[id.RoomID]string
var helpMsgs = map[string]string{
"crypto_help": ": Displays the help message",
"crypto_challenge": "[prefix] : The bot sets a random challenge for the room and echoes it. " +
"The client tested should respond with \"!crypto_response challenge\"." +
"Alternatively the prefix that the challenge will be echoed with can be set.",
"crypto_response": "<challenge> : Should repeat the crypto_challenge's challenge code.",
"crypto_new_session": ": Asks the bot to invalidate its current outgoing group session and create a new one.",
"sas_verify_me": "<device_id> : Asks the bot to start a decimal SAS verification transaction with the sender's specified device.",
"sas_decimal_code": "<device_id> <sas1> <sas2> <sas3> : Sends the device's generated decimal SAS code for the bot to verify, " +
"after a \"!sas_verify_me\" command.",
"request_my_room_key": "<device_id> <sender_key> <session_id> : Asks the bot to request the room key for the current room " +
"and given sender key and session ID from the sender's given device.",
"forward_me_room_key": "<device_id> <sender_key> <session_id> : Asks the bot to send the room key for the current room " +
"and given sender key and session ID to the sender's given device.",
}
// Service represents the Cryptotest service. It has no Config fields.
type Service struct {
types.DefaultService
Rooms []id.RoomID `json:"rooms"`
}
func randomString() (res string) {
for i := 0; i < 10; i++ {
res += string(rune(rand.Intn('Z'-'A') + 'A'))
}
return
}
func (s *Service) inRoom(roomID id.RoomID) bool {
for _, joinedRoomID := range s.Rooms {
if joinedRoomID == roomID {
return true
}
}
return false
}
func (s *Service) handleEventMessage(source mautrix.EventSource, evt *mevt.Event) {
log.Infof("got a %v", evt.Content.AsMessage().Body)
}
func (s *Service) cmdCryptoHelp(roomID id.RoomID) (interface{}, error) {
if s.inRoom(roomID) {
helpTxt := "Supported crypto test methods:\n\n"
for cmd, helpMsg := range helpMsgs {
helpTxt += fmt.Sprintf("!%v %v\n\n", cmd, helpMsg)
}
return mevt.MessageEventContent{MsgType: mevt.MsgText, Body: helpTxt}, nil
}
return nil, nil
}
func (s *Service) cmdCryptoChallenge(roomID id.RoomID, arguments []string) (interface{}, error) {
if s.inRoom(roomID) {
randStr := randomString()
log.Infof("Setting challenge for room %v: %v", roomID, expectedString)
expectedString[roomID] = randStr
prefix := "!challenge"
if len(arguments) > 0 {
prefix = arguments[0]
}
return mevt.MessageEventContent{MsgType: mevt.MsgText, Body: fmt.Sprintf("%v %v", prefix, randStr)}, nil
}
return nil, nil
}
func (s *Service) cmdCryptoResponse(userID id.UserID, roomID id.RoomID, arguments []string) (interface{}, error) {
if s.inRoom(roomID) {
if len(arguments) != 1 {
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: "!crypto_response " + helpMsgs["crypto_response"],
}, nil
}
if arguments[0] == expectedString[roomID] {
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Correct response received from %v", userID.String()),
}, nil
}
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Incorrect response received from %v", userID.String()),
}, nil
}
return nil, nil
}
func (s *Service) cmdCryptoNewSession(botClient *clients.BotClient, roomID id.RoomID) (interface{}, error) {
if s.inRoom(roomID) {
sessionID, err := botClient.InvalidateRoomSession(roomID)
if err != nil {
log.WithField("room_id", roomID).Errorf("Error invalidating session ID: %v", err)
return mevt.MessageEventContent{MsgType: mevt.MsgText, Body: fmt.Sprintf("Error invalidating session ID: %v", sessionID)}, nil
}
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Invalidated previous session ID (%v)", sessionID),
}, nil
}
return nil, nil
}
func (s *Service) cmdSASVerifyMe(botClient *clients.BotClient, roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
if s.inRoom(roomID) {
if len(arguments) != 1 {
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: "sas_verify_me " + helpMsgs["sas_verify_me"],
}, nil
}
deviceID := id.DeviceID(arguments[0])
transaction, err := botClient.StartSASVerification(userID, deviceID)
if err != nil {
log.WithFields(log.Fields{"user_id": userID, "device_id": deviceID}).WithError(err).Error("Error starting SAS verification")
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Error starting SAS verification: %v", err),
}, nil
}
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Started SAS verification with user %v device %v: transaction %v", userID, deviceID, transaction),
}, nil
}
return nil, nil
}
func (s *Service) cmdSASVerifyDecimalCode(botClient *clients.BotClient, roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
if s.inRoom(roomID) {
if len(arguments) != 4 {
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: "sas_decimal_code " + helpMsgs["sas_decimal_code"],
}, nil
}
deviceID := id.DeviceID(arguments[0])
var decimalSAS crypto.DecimalSASData
for i := 0; i < 3; i++ {
sasCode, err := strconv.Atoi(arguments[i+1])
if err != nil {
log.WithFields(log.Fields{"user_id": userID, "device_id": deviceID}).WithError(err).Error("Error reading SAS code")
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Error reading SAS code: %v", err),
}, nil
}
decimalSAS[i] = uint(sasCode)
}
botClient.SubmitDecimalSAS(userID, deviceID, decimalSAS)
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Read SAS code from user %v device %v: %v", userID, deviceID, decimalSAS),
}, nil
}
return nil, nil
}
func (s *Service) cmdRequestRoomKey(botClient *clients.BotClient, roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
if s.inRoom(roomID) {
if len(arguments) != 3 {
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: "request_my_room_key " + helpMsgs["request_my_room_key"],
}, nil
}
deviceID := id.DeviceID(arguments[0])
senderKey := id.SenderKey(arguments[1])
sessionID := id.SessionID(arguments[2])
receivedChan, err := botClient.SendRoomKeyRequest(userID, deviceID, roomID, senderKey, sessionID, time.Minute)
if err != nil {
log.WithFields(log.Fields{
"user_id": userID,
"device_id": deviceID,
"sender_key": senderKey,
"session_id": sessionID,
}).WithError(err).Error("Error requesting room key")
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Error requesting room key for session %v: %v", sessionID, err),
}, nil
}
go func() {
var result string
received := <-receivedChan
if received {
result = "Key received successfully!"
} else {
result = "Key was not received in the time limit"
}
content := mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Room key request for session %v result: %v", sessionID, result),
}
if _, err := botClient.SendMessageEvent(roomID, mevt.EventMessage, content); err != nil {
log.WithFields(log.Fields{
"room_id": roomID,
"content": content,
}).WithError(err).Error("Failed to send room key request result to room")
}
}()
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Sent room key request for session %v to device %v", sessionID, deviceID),
}, nil
}
return nil, nil
}
func (s *Service) cmdForwardRoomKey(botClient *clients.BotClient, roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
if s.inRoom(roomID) {
if len(arguments) != 3 {
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: "forward_me_room_key " + helpMsgs["forward_me_room_key"],
}, nil
}
deviceID := id.DeviceID(arguments[0])
senderKey := id.SenderKey(arguments[1])
sessionID := id.SessionID(arguments[2])
err := botClient.ForwardRoomKeyToDevice(userID, deviceID, roomID, senderKey, sessionID)
if err != nil {
log.WithFields(log.Fields{
"user_id": userID,
"device_id": deviceID,
"sender_key": senderKey,
"session_id": sessionID,
}).WithError(err).Error("Error forwarding room key")
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Error forwarding room key for session %v: %v", sessionID, err),
}, nil
}
return mevt.MessageEventContent{
MsgType: mevt.MsgText,
Body: fmt.Sprintf("Forwarded room key for session %v to device %v", sessionID, deviceID),
}, nil
}
return nil, nil
}
// Commands supported:
// !crypto_help Displays a help string
// !crypto_challenge Sets a challenge for a room which clients should reply to with !crypto_response
// !crypto_response Used by the client to repeat the room challenge
// !crypto_new_session Invalidates the bot's current outgoing session
// !sas_verify_me Asks the bot to verify the sender
// !sas_decimal_code Sends the sender's SAS code to the bot for verification
// !request_my_room_key Asks the bot to request a room key from the sender
// !forward_me_room_key Asks the bot to forward a room key to the sender
// This service can be used for testing other clients by writing the commands above in a room where this service is enabled.
func (s *Service) Commands(cli types.MatrixClient) []types.Command {
botClient := cli.(*clients.BotClient)
return []types.Command{
{
Path: []string{"crypto_help"},
Command: func(roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
return s.cmdCryptoHelp(roomID)
},
},
{
Path: []string{"crypto_challenge"},
Command: func(roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
return s.cmdCryptoChallenge(roomID, arguments)
},
},
{
Path: []string{"crypto_response"},
Command: func(roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
return s.cmdCryptoResponse(userID, roomID, arguments)
},
},
{
Path: []string{"crypto_new_session"},
Command: func(roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
return s.cmdCryptoNewSession(botClient, roomID)
},
},
{
Path: []string{"sas_verify_me"},
Command: func(roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
return s.cmdSASVerifyMe(botClient, roomID, userID, arguments)
},
},
{
Path: []string{"sas_decimal_code"},
Command: func(roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
return s.cmdSASVerifyDecimalCode(botClient, roomID, userID, arguments)
},
},
{
Path: []string{"request_my_room_key"},
Command: func(roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
return s.cmdRequestRoomKey(botClient, roomID, userID, arguments)
},
},
{
Path: []string{"forward_me_room_key"},
Command: func(roomID id.RoomID, userID id.UserID, arguments []string) (interface{}, error) {
return s.cmdForwardRoomKey(botClient, roomID, userID, arguments)
},
},
}
}
// Register registers
func (s *Service) Register(oldService types.Service, client types.MatrixClient) error {
botClient := client.(*clients.BotClient)
botClient.Syncer.(mautrix.ExtensibleSyncer).OnEventType(mevt.EventMessage, s.handleEventMessage)
for _, roomID := range s.Rooms {
if _, err := client.JoinRoom(roomID.String(), "", nil); err != nil {
log.WithFields(log.Fields{
log.ErrorKey: err,
"room_id": roomID,
}).Error("Failed to join room")
}
}
return nil
}
func init() {
expectedString = make(map[id.RoomID]string)
types.RegisterService(func(serviceID string, serviceUserID id.UserID, webhookEndpointURL string) types.Service {
s := &Service{
DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType),
}
return s
})
}
Loading…
Cancel
Save