From 582ce5de49872ca46ed0bd4ae7f57ecd5202ab84 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Mon, 22 Jan 2024 15:22:02 -0700 Subject: [PATCH] verificationhelper/qrcode: begin implementing flow Signed-off-by: Sumner Evans --- client.go | 14 + crypto/verificationhelper/reciprocate.go | 257 +++++++ .../verificationhelper/verificationhelper.go | 655 ++++++++++++++++++ event/content.go | 35 + id/crypto.go | 13 + 5 files changed, 974 insertions(+) create mode 100644 crypto/verificationhelper/reciprocate.go create mode 100644 crypto/verificationhelper/verificationhelper.go diff --git a/client.go b/client.go index 6562559f..c9523190 100644 --- a/client.go +++ b/client.go @@ -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 diff --git a/crypto/verificationhelper/reciprocate.go b/crypto/verificationhelper/reciprocate.go new file mode 100644 index 00000000..e1c5d403 --- /dev/null +++ b/crypto/verificationhelper/reciprocate.go @@ -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 +} diff --git a/crypto/verificationhelper/verificationhelper.go b/crypto/verificationhelper/verificationhelper.go new file mode 100644 index 00000000..6dee8b7b --- /dev/null +++ b/crypto/verificationhelper/verificationhelper.go @@ -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") +} diff --git a/event/content.go b/event/content.go index 0439d9a2..68b8fa03 100644 --- a/event/content.go +++ b/event/content.go @@ -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 +} diff --git a/id/crypto.go b/id/crypto.go index 48a63e78..355a84a8 100644 --- a/id/crypto.go +++ b/id/crypto.go @@ -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