verificationhelper/qrcode: begin implementing flow

Signed-off-by: Sumner Evans <sumner@beeper.com>
This commit is contained in:
Sumner Evans 2024-01-22 15:22:02 -07:00
commit 582ce5de49
No known key found for this signature in database
GPG key ID: 8904527AB50022FD
5 changed files with 974 additions and 0 deletions

View file

@ -35,6 +35,19 @@ type CryptoHelper interface {
Init(context.Context) error
}
type VerificationHelper interface {
Init(context.Context) error
StartVerification(ctx context.Context, to id.UserID) (id.VerificationTransactionID, error)
StartInRoomVerification(ctx context.Context, roomID id.RoomID, to id.UserID) (id.VerificationTransactionID, error)
AcceptVerification(ctx context.Context, txnID id.VerificationTransactionID) error
HandleScannedQRData(ctx context.Context, data []byte) error
ConfirmQRCodeScanned(ctx context.Context, txnID id.VerificationTransactionID) error
StartSAS(ctx context.Context, txnID id.VerificationTransactionID) error
ConfirmSAS(ctx context.Context, txnID id.VerificationTransactionID) error
}
// Deprecated: switch to zerolog
type Logger interface {
Debugfln(message string, args ...interface{})
@ -58,6 +71,7 @@ type Client struct {
Store SyncStore // The thing which can store tokens/ids
StateStore StateStore
Crypto CryptoHelper
Verification VerificationHelper
Log zerolog.Logger
// Deprecated: switch to the zerolog instance in Log

View file

@ -0,0 +1,257 @@
// Copyright (c) 2024 Sumner Evans
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package verificationhelper
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"golang.org/x/exp/slices"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// HandleScannedQRData verifies the keys from a scanned QR code and if
// successful, sends the m.key.verification.start event and
// m.key.verification.done event.
func (vh *VerificationHelper) HandleScannedQRData(ctx context.Context, data []byte) error {
qrCode, err := NewQRCodeFromBytes(data)
if err != nil {
return err
}
log := vh.getLog(ctx).With().
Str("verification_action", "handle scanned QR data").
Stringer("transaction_id", qrCode.TransactionID).
Int("mode", int(qrCode.Mode)).
Logger()
txn, ok := vh.activeTransactions[qrCode.TransactionID]
if !ok {
log.Warn().Msg("Ignoring QR code scan for an unknown transaction")
return nil
} else if txn.VerificationStep != verificationStepReady {
log.Warn().Msg("Ignoring QR code scan for a transaction that is not in the ready state")
return nil
}
// Verify the keys
log.Info().Msg("Verifying keys from QR code")
switch qrCode.Mode {
case QRCodeModeCrossSigning:
// TODO
panic("unimplemented")
// TODO sign their master key
case QRCodeModeSelfVerifyingMasterKeyTrusted:
// The QR was created by a device that trusts the master key, which
// means that we don't trust the key. Key1 is the master key public
// key, and Key2 is what the other device thinks our device key is.
if vh.client.UserID != txn.TheirUser {
return fmt.Errorf("mode %d is only allowed when the other user is the same as the current user", qrCode.Mode)
}
// Verify the master key is correct
crossSigningPubkeys := vh.mach.GetOwnCrossSigningPublicKeys(ctx)
crossSigningMasterKeyBytes, err := base64.RawStdEncoding.DecodeString(crossSigningPubkeys.MasterKey.String())
if err != nil {
return err
}
if bytes.Equal(crossSigningMasterKeyBytes, qrCode.Key1[:]) {
log.Info().Msg("Verified that the other device has the same master key")
} else {
return fmt.Errorf("the master key does not match")
}
// Verify that the device key that the other device things we have is
// correct.
myDevice := vh.mach.OwnIdentity()
myDeviceKeyBytes, err := base64.RawStdEncoding.DecodeString(myDevice.IdentityKey.String())
if err != nil {
return err
}
if bytes.Equal(myDeviceKeyBytes, qrCode.Key2[:]) {
log.Info().Msg("Verified that the other device has the correct key for this device")
} else {
return fmt.Errorf("the other device has the wrong key for this device")
}
case QRCodeModeSelfVerifyingMasterKeyUntrusted:
// The QR was created by a device that does not trust the master key,
// which means that we do trust the master key. Key1 is the other
// device's device key, and Key2 is what the other device thinks the
// master key is.
if vh.client.UserID != txn.TheirUser {
return fmt.Errorf("mode %d is only allowed when the other user is the same as the current user", qrCode.Mode)
}
// Get their device
theirDevice, err := vh.mach.GetOrFetchDevice(ctx, txn.TheirUser, txn.TheirDevice)
if err != nil {
return err
}
// Verify that the other device's key is what we expect.
myDeviceKeyBytes, err := base64.RawStdEncoding.DecodeString(theirDevice.IdentityKey.String())
if err != nil {
return err
}
if bytes.Equal(myDeviceKeyBytes, qrCode.Key1[:]) {
log.Info().Msg("Verified that the other device key is what we expected")
} else {
return fmt.Errorf("the other device's key is not what we expected")
}
// Verify that what they think the master key is is correct.
crossSigningPubkeys := vh.mach.GetOwnCrossSigningPublicKeys(ctx)
crossSigningMasterKeyBytes, err := base64.RawStdEncoding.DecodeString(crossSigningPubkeys.MasterKey.String())
if err != nil {
return err
}
if bytes.Equal(crossSigningMasterKeyBytes, qrCode.Key2[:]) {
log.Info().Msg("Verified that the other device has the correct master key")
} else {
return fmt.Errorf("the master key does not match")
}
// Trust their device
theirDevice.Trust = id.TrustStateVerified
err = vh.mach.CryptoStore.PutDevice(ctx, txn.TheirUser, theirDevice)
if err != nil {
return fmt.Errorf("failed to update device trust state after verifying: %w", err)
}
// TODO Cross-sign their device with the cross-signing key
default:
return fmt.Errorf("unknown QR code mode %d", qrCode.Mode)
}
// Send a m.key.verification.start event with the secret
startEvt := &event.VerificationStartEventContent{
FromDevice: vh.client.DeviceID,
Method: event.VerificationMethodReciprocate,
Secret: qrCode.SharedSecret,
}
err = vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationStart, startEvt)
if err != nil {
return err
}
// Immediately send the m.key.verification.done event, as our side of the
// transaction is done.
err = vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationDone, &event.VerificationDoneEventContent{})
if err != nil {
return err
}
vh.activeTransactionsLock.Lock()
defer vh.activeTransactionsLock.Unlock()
delete(vh.activeTransactions, txn.TransactionID)
// Broadcast that the verification is complete.
vh.verificationDone(ctx, txn.TransactionID)
// TODO do we need to also somehow broadcast that we are now a trusted
// device?
return nil
}
func (vh *VerificationHelper) ConfirmQRCodeScanned(ctx context.Context, txnID id.VerificationTransactionID) error {
log := vh.getLog(ctx).With().
Str("verification_action", "confirm QR code scanned").
Stringer("transaction_id", txnID).
Logger()
txn, ok := vh.activeTransactions[txnID]
if !ok {
log.Warn().Msg("Ignoring QR code scan confirmation for an unknown transaction")
return nil
} else if txn.VerificationStep != verificationStepStarted {
log.Warn().Msg("Ignoring QR code scan confirmation for a transaction that is not in the started state")
return nil
}
log.Info().Msg("Confirming QR code scanned")
// TODO trust the keys somehow
err := vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationDone, &event.VerificationDoneEventContent{})
if err != nil {
return err
}
vh.activeTransactionsLock.Lock()
defer vh.activeTransactionsLock.Unlock()
delete(vh.activeTransactions, txn.TransactionID)
// Broadcast that the verification is complete.
vh.verificationDone(ctx, txn.TransactionID)
// TODO do we need to also somehow broadcast that we are now a trusted
// device?
return nil
}
func (vh *VerificationHelper) generateAndShowQRCode(ctx context.Context, txn *verificationTransaction) error {
log := vh.getLog(ctx).With().
Str("verification_action", "generate and show QR code").
Stringer("transaction_id", txn.TransactionID).
Logger()
if vh.showQRCode == nil || !slices.Contains(txn.SupportedMethods, event.VerificationMethodQRCodeShow) {
log.Warn().Msg("Ignoring QR code generation request as showing a QR code is not enabled")
return nil
}
ownCrossSigningPublicKeys := vh.mach.GetOwnCrossSigningPublicKeys(ctx)
mode := QRCodeModeCrossSigning
if vh.client.UserID == txn.TheirUser {
// This is a self-signing situation.
// TODO determine if it's trusted or not.
mode = QRCodeModeSelfVerifyingMasterKeyUntrusted
}
var key1, key2 []byte
switch mode {
case QRCodeModeCrossSigning:
// Key 1 is the current user's master signing key.
key1 = ownCrossSigningPublicKeys.MasterKey.Bytes()
// Key 2 is the other user's master signing key.
theirSigningKeys, err := vh.mach.GetCrossSigningPublicKeys(ctx, txn.TheirUser)
if err != nil {
return err
}
key2 = theirSigningKeys.MasterKey.Bytes()
case QRCodeModeSelfVerifyingMasterKeyTrusted:
// Key 1 is the current user's master signing key.
key1 = ownCrossSigningPublicKeys.MasterKey.Bytes()
// Key 2 is the other device's key.
theirDevice, err := vh.mach.GetOrFetchDevice(ctx, txn.TheirUser, txn.TheirDevice)
if err != nil {
return err
}
key2 = theirDevice.IdentityKey.Bytes()
case QRCodeModeSelfVerifyingMasterKeyUntrusted:
// Key 1 is the current device's key
key1 = vh.mach.OwnIdentity().IdentityKey.Bytes()
// Key 2 is the master signing key.
key2 = ownCrossSigningPublicKeys.MasterKey.Bytes()
default:
log.Fatal().Str("mode", string(mode)).Msg("Unknown QR code mode")
}
qrCode := NewQRCode(mode, txn.TransactionID, [32]byte(key1), [32]byte(key2))
txn.QRCodeSharedSecret = qrCode.SharedSecret
vh.showQRCode(ctx, txn.TransactionID, qrCode)
return nil
}

View file

@ -0,0 +1,655 @@
// Copyright (c) 2024 Sumner Evans
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package verificationhelper
import (
"bytes"
"context"
"crypto/ecdh"
"errors"
"fmt"
"github.com/rs/zerolog"
"go.mau.fi/util/jsontime"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type verificationStep int
const (
verificationStepRequested verificationStep = iota
verificationStepReady
verificationStepStarted
)
func (step verificationStep) String() string {
switch step {
case verificationStepRequested:
return "requested"
case verificationStepReady:
return "ready"
case verificationStepStarted:
return "started"
default:
return fmt.Sprintf("verificationStep(%d)", step)
}
}
type verificationTransaction struct {
// RoomID is the room ID if the verification is happening in a room or
// empty if it is a to-device verification.
RoomID id.RoomID
// VerificationStep is the current step of the verification flow.
VerificationStep verificationStep
// TransactionID is the ID of the verification transaction.
TransactionID id.VerificationTransactionID
// TheirDevice is the device ID of the device that either made the initial
// request or accepted our request.
TheirDevice id.DeviceID
// TheirUser is the user ID of the other user.
TheirUser id.UserID
// SentToDeviceIDs is a list of devices which the initial request was sent
// to. This is only used for to-device verification requests, and is meant
// to be used to send cancellation requests to all other devices when a
// verification request is accepted via a m.key.verification.ready event.
SentToDeviceIDs []id.DeviceID
// SupportedMethods is a list of verification methods that the other device
// supports.
SupportedMethods []event.VerificationMethod
// QRCodeSharedSecret is the shared secret that was encoded in the QR code
// that we showed.
QRCodeSharedSecret []byte
StartedByUs bool // Whether the verification was started by us
StartEventContent *event.VerificationStartEventContent // The m.key.verification.start event content
Commitment []byte // The commitment from the m.key.verification.accept event
EphemeralKey *ecdh.PrivateKey // The ephemeral key
EphemeralPublicKeyShared bool // Whether this device's ephemeral public key has been shared
OtherPublicKey *ecdh.PublicKey // The other device's ephemeral public key
}
// RequiredCallbacks is an interface representing the callbacks required for
// the [VerificationHelper].
type RequiredCallbacks interface {
// VerificationRequested is called when a verification request is received
// from another device.
VerificationRequested(ctx context.Context, txnID id.VerificationTransactionID, from id.UserID)
// VerificationError is called when an error occurs during the verification
// process.
VerificationError(ctx context.Context, txnID id.VerificationTransactionID, err error)
// VerificationCancelled is called when the verification is cancelled.
VerificationCancelled(ctx context.Context, txnID id.VerificationTransactionID, code event.VerificationCancelCode, reason string)
// VerificationDone is called when the verification is done.
VerificationDone(ctx context.Context, txnID id.VerificationTransactionID)
}
type showSASCallbacks interface {
// ShowSAS is called when the SAS verification has generated a short
// authentication string to show. It is guaranteed that either the emojis
// list, or the decimals list, or both will be present.
ShowSAS(ctx context.Context, txnID id.VerificationTransactionID, emojis []rune, decimals []int)
}
type showQRCodeCallbacks interface {
// ShowQRCode is called when the verification has been accepted and a QR
// code should be shown to the user.
ShowQRCode(ctx context.Context, txnID id.VerificationTransactionID, qrCode *QRCode)
// QRCodeScanned is called when the other user has scanned the QR code and
// sent the m.key.verification.start event.
QRCodeScanned(ctx context.Context, txnID id.VerificationTransactionID)
}
type VerificationHelper struct {
client *mautrix.Client
mach *crypto.OlmMachine
activeTransactions map[id.VerificationTransactionID]*verificationTransaction
activeTransactionsLock sync.Mutex
supportedMethods []event.VerificationMethod
verificationRequested func(ctx context.Context, txnID id.VerificationTransactionID, from id.UserID)
verificationError func(ctx context.Context, txnID id.VerificationTransactionID, err error)
verificationCancelled func(ctx context.Context, txnID id.VerificationTransactionID, code event.VerificationCancelCode, reason string)
verificationDone func(ctx context.Context, txnID id.VerificationTransactionID)
showSAS func(ctx context.Context, txnID id.VerificationTransactionID, emojis []rune, decimals []int)
showQRCode func(ctx context.Context, txnID id.VerificationTransactionID, qrCode *QRCode)
qrCodeScaned func(ctx context.Context, txnID id.VerificationTransactionID)
}
var _ mautrix.VerificationHelper = (*VerificationHelper)(nil)
func NewVerificationHelper(client *mautrix.Client, mach *crypto.OlmMachine, callbacks any, supportsScan bool) *VerificationHelper {
if client.Crypto == nil {
panic("client.Crypto is nil")
}
helper := VerificationHelper{
client: client,
mach: mach,
activeTransactions: map[id.VerificationTransactionID]*verificationTransaction{},
}
if c, ok := callbacks.(RequiredCallbacks); !ok {
panic("callbacks must implement VerificationRequested")
} else {
helper.verificationRequested = c.VerificationRequested
helper.verificationError = func(ctx context.Context, txnID id.VerificationTransactionID, err error) {
zerolog.Ctx(ctx).Err(err).Msg("Verification error")
c.VerificationError(ctx, txnID, err)
}
helper.verificationCancelled = c.VerificationCancelled
helper.verificationDone = c.VerificationDone
}
if c, ok := callbacks.(showEmojiCallbacks); ok {
helper.supportedMethods = append(helper.supportedMethods, event.VerificationMethodSAS)
helper.showEmojis = c.ShowEmojis
}
if c, ok := callbacks.(showDecimalCallbacks); ok {
helper.supportedMethods = append(helper.supportedMethods, event.VerificationMethodSAS)
helper.showDecimal = c.ShowDecimal
}
if c, ok := callbacks.(showQRCodeCallbacks); ok {
helper.supportedMethods = append(helper.supportedMethods,
event.VerificationMethodQRCodeShow, event.VerificationMethodReciprocate)
helper.showQRCode = c.ShowQRCode
helper.qrCodeScaned = c.QRCodeScanned
}
if supportsScan {
helper.supportedMethods = append(helper.supportedMethods,
event.VerificationMethodQRCodeScan, event.VerificationMethodReciprocate)
}
slices.Sort(helper.supportedMethods)
helper.supportedMethods = slices.Compact(helper.supportedMethods)
return &helper
}
func (vh *VerificationHelper) getLog(ctx context.Context) *zerolog.Logger {
logger := vh.client.Log.With().
Any("supported_methods", vh.supportedMethods).
Str("component", "verification").
Logger()
return &logger
}
// Init initializes the verification helper by adding the necessary event
// handlers to the syncer.
func (vh *VerificationHelper) Init(ctx context.Context) error {
if vh == nil {
return fmt.Errorf("verification helper is nil")
}
syncer, ok := vh.client.Syncer.(mautrix.ExtensibleSyncer)
if !ok {
return fmt.Errorf("the client syncer must implement ExtensibleSyncer")
}
// Event handlers for verification requests. These are special since we do
// not need to check that the transaction ID is known.
syncer.OnEventType(event.ToDeviceVerificationRequest, vh.onVerificationRequest)
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
if evt.Content.AsMessage().MsgType == event.MsgVerificationRequest {
vh.onVerificationRequest(ctx, evt)
}
})
// Wrapper for the event handlers to check that the transaction ID is known
// and ignore the event if it isn't.
wrapHandler := func(callback func(context.Context, *verificationTransaction, *event.Event)) func(context.Context, *event.Event) {
return func(ctx context.Context, evt *event.Event) {
log := vh.getLog(ctx).With().
Str("verification_action", "check transaction ID").
Stringer("sender", evt.Sender).
Stringer("room_id", evt.RoomID).
Stringer("event_id", evt.ID).
Logger()
var transactionID id.VerificationTransactionID
if evt.ID != "" {
transactionID = id.VerificationTransactionID(evt.ID)
} else {
txnID, ok := evt.Content.Raw["transaction_id"].(string)
if !ok {
log.Warn().Msg("Ignoring verification event without a transaction ID")
return
}
transactionID = id.VerificationTransactionID(txnID)
}
vh.activeTransactionsLock.Lock()
txn, ok := vh.activeTransactions[transactionID]
vh.activeTransactionsLock.Unlock()
if !ok {
log.Warn().
Stringer("transaction_id", transactionID).
Msg("Ignoring verification event for an unknown transaction")
txn = &verificationTransaction{
RoomID: evt.RoomID,
TheirUser: evt.Sender,
}
txn.TransactionID = evt.Content.Parsed.(event.VerificationTransactionable).GetTransactionID()
if txn.TransactionID == "" {
txn.TransactionID = id.VerificationTransactionID(evt.ID)
}
if fromDevice, ok := evt.Content.Raw["from_device"]; ok {
txn.TheirDevice = id.DeviceID(fromDevice.(string))
}
cancelEvt := event.VerificationCancelEventContent{
Code: event.VerificationCancelCodeUnknownTransaction,
Reason: "The transaction ID was not recognized.",
}
err := vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationCancel, &cancelEvt)
if err != nil {
log.Err(err).Msg("Failed to send cancellation event")
}
vh.verificationCancelled(ctx, txn.TransactionID, cancelEvt.Code, cancelEvt.Reason)
return
}
logCtx := vh.getLog(ctx).With().
Stringer("transaction_id", transactionID).
Stringer("transaction_step", txn.VerificationStep).
Stringer("sender", evt.Sender)
if evt.RoomID != "" {
logCtx = logCtx.
Stringer("room_id", evt.RoomID).
Stringer("event_id", evt.ID)
}
callback(logCtx.Logger().WithContext(ctx), txn, evt)
}
}
// Event handlers for the to-device verification events.
syncer.OnEventType(event.ToDeviceVerificationReady, wrapHandler(vh.onVerificationReady))
syncer.OnEventType(event.ToDeviceVerificationStart, wrapHandler(vh.onVerificationStart))
syncer.OnEventType(event.ToDeviceVerificationDone, wrapHandler(vh.onVerificationDone))
syncer.OnEventType(event.ToDeviceVerificationCancel, wrapHandler(vh.onVerificationCancel))
syncer.OnEventType(event.ToDeviceVerificationAccept, wrapHandler(vh.onVerificationAccept)) // SAS
syncer.OnEventType(event.ToDeviceVerificationKey, wrapHandler(vh.onVerificationKey)) // SAS
syncer.OnEventType(event.ToDeviceVerificationMAC, wrapHandler(vh.onVerificationMAC)) // SAS
// Event handlers for the in-room verification events.
syncer.OnEventType(event.InRoomVerificationReady, wrapHandler(vh.onVerificationReady))
syncer.OnEventType(event.InRoomVerificationStart, wrapHandler(vh.onVerificationStart))
syncer.OnEventType(event.InRoomVerificationDone, wrapHandler(vh.onVerificationDone))
syncer.OnEventType(event.InRoomVerificationCancel, wrapHandler(vh.onVerificationCancel))
syncer.OnEventType(event.InRoomVerificationAccept, wrapHandler(vh.onVerificationAccept)) // SAS
syncer.OnEventType(event.InRoomVerificationKey, wrapHandler(vh.onVerificationKey)) // SAS
syncer.OnEventType(event.InRoomVerificationMAC, wrapHandler(vh.onVerificationMAC)) // SAS
return nil
}
// StartVerification starts an interactive verification flow with the given
// user via a to-device event.
func (vh *VerificationHelper) StartVerification(ctx context.Context, to id.UserID) (id.VerificationTransactionID, error) {
txnID := id.NewVerificationTransactionID()
devices, err := vh.mach.CryptoStore.GetDevices(ctx, to)
if err != nil {
return "", fmt.Errorf("failed to get devices for user: %w", err)
}
vh.getLog(ctx).Info().
Str("verification_action", "start verification").
Stringer("transaction_id", txnID).
Stringer("to", to).
Any("device_ids", maps.Keys(devices)).
Msg("Sending verification request")
content := &event.Content{
Parsed: &event.VerificationRequestEventContent{
ToDeviceVerificationEvent: event.ToDeviceVerificationEvent{TransactionID: txnID},
FromDevice: vh.client.DeviceID,
Methods: vh.supportedMethods,
Timestamp: jsontime.UnixMilliNow(),
},
}
req := mautrix.ReqSendToDevice{Messages: map[id.UserID]map[id.DeviceID]*event.Content{to: {}}}
for deviceID := range devices {
if deviceID == vh.client.DeviceID {
// Don't ever send the event to the current device. We are likely
// trying to send a verification request to our other devices.
continue
}
req.Messages[to][deviceID] = content
}
_, err = vh.client.SendToDevice(ctx, event.ToDeviceVerificationRequest, &req)
if err != nil {
return "", fmt.Errorf("failed to send verification request: %w", err)
}
vh.activeTransactionsLock.Lock()
defer vh.activeTransactionsLock.Unlock()
vh.activeTransactions[txnID] = &verificationTransaction{
VerificationStep: verificationStepRequested,
TransactionID: txnID,
TheirUser: to,
SentToDeviceIDs: maps.Keys(devices),
}
return txnID, nil
}
// StartVerification starts an interactive verification flow with the given
// user in the given room.
func (vh *VerificationHelper) StartInRoomVerification(ctx context.Context, roomID id.RoomID, to id.UserID) (id.VerificationTransactionID, error) {
log := vh.getLog(ctx).With().
Str("verification_action", "start in-room verification").
Stringer("room_id", roomID).
Stringer("to", to).
Logger()
log.Info().Msg("Sending verification request")
content := event.MessageEventContent{
MsgType: event.MsgVerificationRequest,
Body: "Alice is requesting to verify your device, but your client does not support verification, so you may need to use a different verification method.",
FromDevice: vh.client.DeviceID,
Methods: vh.supportedMethods,
To: to,
}
encryptedContent, err := vh.client.Crypto.Encrypt(ctx, roomID, event.EventMessage, &content)
if err != nil {
return "", fmt.Errorf("failed to encrypt verification request: %w", err)
}
resp, err := vh.client.SendMessageEvent(ctx, roomID, event.EventMessage, encryptedContent)
if err != nil {
return "", fmt.Errorf("failed to send verification request: %w", err)
}
txnID := id.VerificationTransactionID(resp.EventID)
log.Info().Stringer("transaction_id", txnID).Msg("Got a transaction ID for the verification request")
vh.activeTransactionsLock.Lock()
defer vh.activeTransactionsLock.Unlock()
vh.activeTransactions[txnID] = &verificationTransaction{
RoomID: roomID,
VerificationStep: verificationStepRequested,
TransactionID: txnID,
TheirUser: to,
}
return txnID, nil
}
// AcceptVerification accepts a verification request. The transaction ID should
// be the transaction ID of a verification request that was received via the
// [RequiredCallbacks.VerificationRequested] callback.
func (vh *VerificationHelper) AcceptVerification(ctx context.Context, txnID id.VerificationTransactionID) error {
log := vh.getLog(ctx).With().
Str("verification_action", "accept verification").
Stringer("transaction_id", txnID).
Logger()
txn, ok := vh.activeTransactions[txnID]
if !ok {
return fmt.Errorf("unknown transaction ID")
}
log.Info().Msg("Sending ready event")
readyEvt := &event.VerificationReadyEventContent{
FromDevice: vh.client.DeviceID,
Methods: vh.supportedMethods,
}
err := vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationReady, readyEvt)
if err != nil {
return err
}
txn.VerificationStep = verificationStepReady
return vh.generateAndShowQRCode(ctx, txn)
}
// sendVerificationEvent sends a verification event to the other user's device
// setting the m.relates_to or transaction ID as necessary.
//
// Notes:
//
// - "content" must implement [event.Relatable] and
// [event.VerificationTransactionable].
// - evtType can be either the to-device or in-room version of the event type
// as it is always stringified.
func (vh *VerificationHelper) sendVerificationEvent(ctx context.Context, txn *verificationTransaction, evtType event.Type, content any) error {
if txn.RoomID != "" {
content.(event.Relatable).SetRelatesTo(&event.RelatesTo{Type: event.RelReference, EventID: id.EventID(txn.TransactionID)})
_, err := vh.client.SendMessageEvent(ctx, txn.RoomID, evtType, &event.Content{
Parsed: content,
})
if err != nil {
return fmt.Errorf("failed to send start event: %w", err)
}
} else {
content.(event.VerificationTransactionable).SetTransactionID(txn.TransactionID)
req := mautrix.ReqSendToDevice{Messages: map[id.UserID]map[id.DeviceID]*event.Content{
txn.TheirUser: {
txn.TheirDevice: &event.Content{Parsed: content},
},
}}
_, err := vh.client.SendToDevice(ctx, evtType, &req)
if err != nil {
return fmt.Errorf("failed to send start event: %w", err)
}
}
return nil
}
func (vh *VerificationHelper) onVerificationRequest(ctx context.Context, evt *event.Event) {
logCtx := vh.getLog(ctx).With().
Str("verification_action", "verification request").
Stringer("sender", evt.Sender)
if evt.RoomID != "" {
logCtx = logCtx.
Stringer("room_id", evt.RoomID).
Stringer("event_id", evt.ID)
}
log := logCtx.Logger()
var verificationRequest *event.VerificationRequestEventContent
switch evt.Type {
case event.EventMessage:
to := evt.Content.AsMessage().To
if to != vh.client.UserID {
log.Info().Stringer("to", to).Msg("Ignoring verification request for another user")
return
}
verificationRequest = event.VerificationRequestEventContentFromMessage(evt)
case event.ToDeviceVerificationRequest:
verificationRequest = evt.Content.AsVerificationRequest()
default:
log.Warn().Str("type", evt.Type.Type).Msg("Ignoring verification request of unknown type")
return
}
if verificationRequest.FromDevice == vh.client.DeviceID {
log.Warn().Msg("Ignoring verification request from our own device. Why did it even get sent to us?")
return
}
if verificationRequest.TransactionID == "" {
log.Warn().Msg("Ignoring verification request without a transaction ID")
return
}
log = log.With().Any("requested_methods", verificationRequest.Methods).Logger()
ctx = log.WithContext(ctx)
log.Info().Msg("Received verification request")
vh.activeTransactionsLock.Lock()
_, ok := vh.activeTransactions[verificationRequest.TransactionID]
if ok {
vh.activeTransactionsLock.Unlock()
log.Info().Msg("Ignoring verification request for an already active transaction")
return
}
vh.activeTransactions[verificationRequest.TransactionID] = &verificationTransaction{
RoomID: evt.RoomID,
VerificationStep: verificationStepRequested,
TransactionID: verificationRequest.TransactionID,
TheirDevice: verificationRequest.FromDevice,
TheirUser: evt.Sender,
SupportedMethods: verificationRequest.Methods,
}
vh.activeTransactionsLock.Unlock()
vh.verificationRequested(ctx, verificationRequest.TransactionID, evt.Sender)
}
func (vh *VerificationHelper) onVerificationReady(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
log := vh.getLog(ctx).With().
Str("verification_action", "verification ready").
Logger()
if txn.VerificationStep != verificationStepRequested {
log.Warn().Msg("Ignoring verification ready event for a transaction that is not in the requested state")
return
}
vh.activeTransactionsLock.Lock()
defer vh.activeTransactionsLock.Unlock()
readyEvt := evt.Content.AsVerificationReady()
// Update the transaction state.
txn.VerificationStep = verificationStepReady
txn.TheirDevice = readyEvt.FromDevice
txn.SupportedMethods = readyEvt.Methods
// If we sent this verification request, send cancellations to all of the
// other devices.
if len(txn.SentToDeviceIDs) > 0 {
content := &event.Content{
Parsed: &event.VerificationCancelEventContent{
ToDeviceVerificationEvent: event.ToDeviceVerificationEvent{TransactionID: txn.TransactionID},
Code: event.VerificationCancelCodeAccepted,
Reason: "The verification was accepted on another device.",
},
}
devices, err := vh.mach.CryptoStore.GetDevices(ctx, txn.TheirUser)
if err != nil {
vh.verificationError(ctx, txn.TransactionID, fmt.Errorf("failed to get devices for %s: %w", txn.TheirUser, err))
return
}
req := mautrix.ReqSendToDevice{Messages: map[id.UserID]map[id.DeviceID]*event.Content{txn.TheirUser: {}}}
for deviceID := range devices {
if deviceID == txn.TheirDevice {
// Don't ever send a cancellation to the device that accepted
// the request.
continue
}
req.Messages[txn.TheirUser][deviceID] = content
}
_, err = vh.client.SendToDevice(ctx, event.ToDeviceVerificationRequest, &req)
if err != nil {
log.Warn().Err(err).Msg("Failed to send cancellation requests")
}
}
err := vh.generateAndShowQRCode(ctx, txn)
if err != nil {
vh.verificationError(ctx, txn.TransactionID, fmt.Errorf("failed to generate and show QR code: %w", err))
}
}
func (vh *VerificationHelper) onVerificationStart(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
startEvt := evt.Content.AsVerificationStart()
log := vh.getLog(ctx).With().
Str("verification_action", "verification start").
Str("method", string(startEvt.Method)).
Logger()
if txn.VerificationStep != verificationStepReady {
log.Warn().Msg("Ignoring verification start event for a transaction that is not in the ready state")
return
}
vh.activeTransactionsLock.Lock()
defer vh.activeTransactionsLock.Unlock()
txn.VerificationStep = verificationStepStarted
switch startEvt.Method {
case event.VerificationMethodSAS:
// TODO
log.Info().Msg("Received SAS verification start event")
err := vh.onVerificationStartSAS(ctx, txn, evt)
if err != nil {
// TODO should we cancel on all errors?
vh.verificationError(ctx, txn.TransactionID, fmt.Errorf("failed to handle SAS verification start: %w", err))
}
case event.VerificationMethodReciprocate:
log.Info().Msg("Received reciprocate start event")
if !bytes.Equal(txn.QRCodeSharedSecret, startEvt.Secret) {
vh.verificationError(ctx, txn.TransactionID, errors.New("reciprocated shared secret does not match"))
return
}
vh.qrCodeScaned(ctx, txn.TransactionID)
default:
// Note that we should never get m.qr_code.show.v1 or m.qr_code.scan.v1
// here, since the start command for scanning and showing QR codes
// should be of type m.reciprocate.v1.
log.Error().Str("method", string(startEvt.Method)).Msg("Unsupported verification method in start event")
}
}
func (vh *VerificationHelper) onVerificationDone(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
vh.getLog(ctx).Info().
Str("verification_action", "done").
Stringer("transaction_id", txn.TransactionID).
Msg("Verification done")
vh.activeTransactionsLock.Lock()
defer vh.activeTransactionsLock.Unlock()
delete(vh.activeTransactions, txn.TransactionID)
}
func (vh *VerificationHelper) onVerificationCancel(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
cancelEvt := evt.Content.AsVerificationCancel()
vh.getLog(ctx).Info().
Str("verification_action", "cancel").
Stringer("transaction_id", txn.TransactionID).
Str("cancel_code", string(cancelEvt.Code)).
Str("reason", cancelEvt.Reason).
Msg("Verification was cancelled")
vh.activeTransactionsLock.Lock()
defer vh.activeTransactionsLock.Unlock()
delete(vh.activeTransactions, txn.TransactionID)
vh.verificationCancelled(ctx, txn.TransactionID, cancelEvt.Code, cancelEvt.Reason)
}
// SAS verification events
func (vh *VerificationHelper) onVerificationAccept(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
// TODO
vh.getLog(ctx).Error().Any("evt", evt).Msg("ACCEPT UNIMPLEMENTED")
}
func (vh *VerificationHelper) onVerificationKey(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
// TODO
vh.getLog(ctx).Error().Any("evt", evt).Msg("KEY UNIMPLEMENTED")
}
func (vh *VerificationHelper) onVerificationMAC(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
// TODO
vh.getLog(ctx).Error().Any("evt", evt).Msg("MAC UNIMPLEMENTED")
}

View file

@ -513,3 +513,38 @@ func (content *Content) AsModPolicy() *ModPolicyContent {
}
return casted
}
func (content *Content) AsVerificationRequest() *VerificationRequestEventContent {
casted, ok := content.Parsed.(*VerificationRequestEventContent)
if !ok {
return &VerificationRequestEventContent{}
}
return casted
}
func (content *Content) AsVerificationReady() *VerificationReadyEventContent {
casted, ok := content.Parsed.(*VerificationReadyEventContent)
if !ok {
return &VerificationReadyEventContent{}
}
return casted
}
func (content *Content) AsVerificationStart() *VerificationStartEventContent {
casted, ok := content.Parsed.(*VerificationStartEventContent)
if !ok {
return &VerificationStartEventContent{}
}
return casted
}
func (content *Content) AsVerificationDone() *VerificationDoneEventContent {
casted, ok := content.Parsed.(*VerificationDoneEventContent)
if !ok {
return &VerificationDoneEventContent{}
}
return casted
}
func (content *Content) AsVerificationCancel() *VerificationCancelEventContent {
casted, ok := content.Parsed.(*VerificationCancelEventContent)
if !ok {
return &VerificationCancelEventContent{}
}
return casted
}

View file

@ -7,6 +7,7 @@
package id
import (
"encoding/base64"
"fmt"
"strings"
@ -74,6 +75,12 @@ func (ed25519 Ed25519) String() string {
return string(ed25519)
}
func (ed25519 Ed25519) Bytes() []byte {
val, _ := base64.RawStdEncoding.DecodeString(string(ed25519))
// TODO handle errors
return val
}
func (ed25519 Ed25519) Fingerprint() string {
spacedSigningKey := make([]byte, len(ed25519)+(len(ed25519)-1)/4)
var ptr = 0
@ -97,6 +104,12 @@ func (curve25519 Curve25519) String() string {
return string(curve25519)
}
func (curve25519 Curve25519) Bytes() []byte {
val, _ := base64.RawStdEncoding.DecodeString(string(curve25519))
// TODO handle errors
return val
}
// A DeviceID is an arbitrary string that references a specific device.
type DeviceID string