Browse Source

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

Signed-off-by: Nikos Filippakis <me@nfil.dev>
pull/333/head
Nikos Filippakis 4 years ago
parent
commit
facd5c40f4
  1. 17
      README.md
  2. 20
      api/api.go
  3. 53
      api/handlers/client.go
  4. 78
      clients/bot_client.go
  5. 1
      clients/clients.go
  6. 48
      clients/clients_test.go
  7. 2
      go.mod
  8. 2
      goneb.go

17
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.

20
api/api.go

@ -80,6 +80,18 @@ type ClientConfig struct {
DisplayName 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. @alice:matrix.org
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.
// They are created for use with ConfigFile.
type Session struct {
@ -132,6 +144,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")
}
if err := body.Check(); err != nil {
return util.MessageResponse(400, "Error parsing client config")
}
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{}{},
}
}

78
clients/bot_client.go

@ -1,6 +1,7 @@
package clients
import (
"sync"
"time"
"github.com/matrix-org/go-neb/api"
@ -9,6 +10,7 @@ import (
log "github.com/sirupsen/logrus"
"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"
)
@ -18,9 +20,10 @@ import (
// 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
}
// InitOlmMachine initializes a BotClient's internal OlmMachine given a client object and a Neb store,
@ -54,6 +57,9 @@ func (botClient *BotClient) InitOlmMachine(client *mautrix.Client, nebStore *mat
botClient.stateStore = &NebStateStore{&nebStore.InMemoryStore}
olmMachine := crypto.NewOlmMachine(client, cryptoLogger, cryptoStore, botClient.stateStore)
olmMachine.AcceptVerificationFrom = func(_ string, _ *crypto.DeviceIdentity) (crypto.VerificationRequestResponse, crypto.VerificationHooks) {
return crypto.AcceptRequest, botClient
}
if err = olmMachine.Load(); err != nil {
return
}
@ -142,3 +148,69 @@ 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) {
}
// OnSuccess is called when a SAS verification is successful.
func (botClient *BotClient) OnSuccess() {
}

1
clients/clients.go

@ -340,6 +340,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
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
goneb.go

@ -189,6 +189,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 != "" {

Loading…
Cancel
Save