From 5f999121f41febc240ae594512785cffe7dbd5b1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 13 Sep 2020 00:13:08 +0300 Subject: [PATCH] Add broken in-room verification and other stuff --- go.mod | 2 +- go.sum | 2 + interface/matrix.go | 1 + matrix/matrix.go | 26 +++++-- matrix/rooms/room.go | 3 +- ui/command-processor.go | 24 +++--- ui/crypto-commands.go | 153 +++++++++++++++++++++++---------------- ui/no-crypto-commands.go | 1 + ui/verification-modal.go | 3 +- 9 files changed, 131 insertions(+), 84 deletions(-) diff --git a/go.mod b/go.mod index e6933d4..94d002a 100644 --- a/go.mod +++ b/go.mod @@ -29,4 +29,4 @@ require ( ) //replace maunium.net/go/mautrix => ../mautrix-go -replace maunium.net/go/mautrix => github.com/nikofil/mautrix-go v0.5.2-0.20200911234625-a585557b3750 +replace maunium.net/go/mautrix => github.com/nikofil/mautrix-go v0.5.2-0.20200912211003-156aad4c3f86 diff --git a/go.sum b/go.sum index 7bdf4b8..5b16ba3 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/nikofil/mautrix-go v0.5.2-0.20200911232449-6010305aed05 h1:WFyJHdXasA github.com/nikofil/mautrix-go v0.5.2-0.20200911232449-6010305aed05/go.mod h1:xd0D0ekVts/UDBbjeDSs4wGlBfcarJDg0MMhVgHbxhs= github.com/nikofil/mautrix-go v0.5.2-0.20200911234625-a585557b3750 h1:3eUgrAhYHnI0HidL2uO1uga8dpnqpAXIN0DB7uWc95Y= github.com/nikofil/mautrix-go v0.5.2-0.20200911234625-a585557b3750/go.mod h1:xd0D0ekVts/UDBbjeDSs4wGlBfcarJDg0MMhVgHbxhs= +github.com/nikofil/mautrix-go v0.5.2-0.20200912211003-156aad4c3f86 h1:cVz8JHJyGRwve5EPDN7X6pUsP3/6Dy2c7gj7RCkZa50= +github.com/nikofil/mautrix-go v0.5.2-0.20200912211003-156aad4c3f86/go.mod h1:xd0D0ekVts/UDBbjeDSs4wGlBfcarJDg0MMhVgHbxhs= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/interface/matrix.go b/interface/matrix.go index b2ae4a4..9430621 100644 --- a/interface/matrix.go +++ b/interface/matrix.go @@ -83,6 +83,7 @@ type Crypto interface { Load() error FlushStore() error ProcessSyncResponse(resp *mautrix.RespSync, since string) + ProcessInRoomVerification(evt *event.Event) error HandleMemberEvent(*event.Event) DecryptMegolmEvent(*event.Event) (*event.Event, error) EncryptMegolmEvent(id.RoomID, event.Type, interface{}) (*event.EncryptedEventContent, error) diff --git a/matrix/matrix.go b/matrix/matrix.go index c1a93cd..383b1d4 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -584,7 +584,16 @@ func (c *Container) HandleEncrypted(source mautrix.EventSource, mxEvent *event.E c.HandleMessage(source, mxEvent) return } - c.HandleMessage(source, evt) + if evt.Type.IsInRoomVerification() { + err := c.crypto.ProcessInRoomVerification(evt) + if err != nil { + debug.Printf("[Crypto/Error] Failed to process in-room verification event %s of type %s: %v", evt.ID, evt.Type.String(), err) + } else { + debug.Printf("[Crypto/Debug] Processed in-room verification event %s of type %s", evt.ID, evt.Type.String()) + } + } else { + c.HandleMessage(source, evt) + } } // HandleMessage is the event handler for the m.room.message timeline event. @@ -743,14 +752,14 @@ func (c *Container) HandleReadReceipt(source mautrix.EventSource, evt *event.Eve } } -func (c *Container) parseDirectChatInfo(evt *event.Event) map[*rooms.Room]bool { - directChats := make(map[*rooms.Room]bool) - for _, roomIDList := range *evt.Content.AsDirectChats() { +func (c *Container) parseDirectChatInfo(evt *event.Event) map[*rooms.Room]id.UserID { + directChats := make(map[*rooms.Room]id.UserID) + for userID, roomIDList := range *evt.Content.AsDirectChats() { for _, roomID := range roomIDList { // TODO we shouldn't create direct chat rooms that we aren't in room := c.GetOrCreateRoom(roomID) if room != nil && !room.HasLeft { - directChats[room] = true + directChats[room] = userID } } } @@ -760,9 +769,10 @@ func (c *Container) parseDirectChatInfo(evt *event.Event) map[*rooms.Room]bool { func (c *Container) HandleDirectChatInfo(_ mautrix.EventSource, evt *event.Event) { directChats := c.parseDirectChatInfo(evt) for _, room := range c.config.Rooms.Map { - shouldBeDirect := directChats[room] - if shouldBeDirect != room.IsDirect { - room.IsDirect = shouldBeDirect + userID, isDirect := directChats[room] + if isDirect != room.IsDirect { + room.IsDirect = isDirect + room.OtherUser = userID if c.config.AuthCache.InitialSyncDone { c.ui.MainView().UpdateTags(room) } diff --git a/matrix/rooms/room.go b/matrix/rooms/room.go index d5d1d8f..40913cf 100644 --- a/matrix/rooms/room.go +++ b/matrix/rooms/room.go @@ -93,7 +93,8 @@ type Room struct { highlightCache *bool lastMarkedRead id.EventID // Whether or not this room is marked as a direct chat. - IsDirect bool + IsDirect bool + OtherUser id.UserID // List of tags given to this room. RawTags []RoomTag diff --git a/ui/command-processor.go b/ui/command-processor.go index da8388a..88dfafd 100644 --- a/ui/command-processor.go +++ b/ui/command-processor.go @@ -109,17 +109,18 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "cs": {"cross-signing"}, }, autocompleters: map[string]CommandAutocompleter{ - "devices": autocompleteDevice, - "device": autocompleteDevice, - "verify": autocompleteDevice, - "unverify": autocompleteDevice, - "blacklist": autocompleteDevice, - "upload": autocompleteFile, - "download": autocompleteFile, - "open": autocompleteFile, - "import": autocompleteFile, - "export": autocompleteFile, - "export-room": autocompleteFile, + "devices": autocompleteUser, + "device": autocompleteDevice, + "verify": autocompleteUser, + "verify-device": autocompleteDevice, + "unverify": autocompleteDevice, + "blacklist": autocompleteDevice, + "upload": autocompleteFile, + "download": autocompleteFile, + "open": autocompleteFile, + "import": autocompleteFile, + "export": autocompleteFile, + "export-room": autocompleteFile, }, commands: map[string]CommandHandler{ "unknown-command": cmdUnknownCommand, @@ -167,6 +168,7 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "fingerprint": cmdFingerprint, "devices": cmdDevices, + "verify-device": cmdVerifyDevice, "verify": cmdVerify, "device": cmdDevice, "unverify": cmdUnverify, diff --git a/ui/crypto-commands.go b/ui/crypto-commands.go index 5f72d40..8968392 100644 --- a/ui/crypto-commands.go +++ b/ui/crypto-commands.go @@ -81,15 +81,20 @@ func autocompleteDeviceDeviceID(cmd *CommandAutocomplete) (completions []string, return } +func autocompleteUser(cmd *CommandAutocomplete) ([]string, string) { + if len(cmd.Args) == 1 && !unicode.IsSpace(rune(cmd.RawArgs[len(cmd.RawArgs)-1])) { + return autocompleteDeviceUserID(cmd) + } + return []string{}, "" +} + func autocompleteDevice(cmd *CommandAutocomplete) ([]string, string) { if len(cmd.Args) == 0 { return []string{}, "" } else if len(cmd.Args) == 1 && !unicode.IsSpace(rune(cmd.RawArgs[len(cmd.RawArgs)-1])) { return autocompleteDeviceUserID(cmd) - } else if cmd.Command != "devices" { - return autocompleteDeviceDeviceID(cmd) } - return []string{}, "" + return autocompleteDeviceDeviceID(cmd) } func getDevice(cmd *Command) *crypto.DeviceIdentity { @@ -138,7 +143,11 @@ func cmdDevices(cmd *Command) { } var buf strings.Builder for _, device := range devices { - _, _ = fmt.Fprintf(&buf, "%s (%s) - %s\n Fingerprint: %s\n", device.DeviceID, device.Name, device.Trust.String(), device.Fingerprint()) + trust := device.Trust.String() + if device.Trust == crypto.TrustStateUnset && mach.IsDeviceTrusted(device) { + trust = "verified (transitive)" + } + _, _ = fmt.Fprintf(&buf, "%s (%s) - %s\n Fingerprint: %s\n", device.DeviceID, device.Name, trust, device.Fingerprint()) } resp := buf.String() cmd.Reply("%s", resp[:len(resp)-1]) @@ -153,13 +162,28 @@ func cmdDevice(cmd *Command) { if device.Deleted { deviceType = "Deleted device" } + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + trustState := device.Trust.String() + if device.Trust == crypto.TrustStateUnset && mach.IsDeviceTrusted(device) { + trustState = "verified (transitive)" + } cmd.Reply("%s %s of %s\nFingerprint: %s\nIdentity key: %s\nDevice name: %s\nTrust state: %s", deviceType, device.DeviceID, device.UserID, device.Fingerprint(), device.IdentityKey, - device.Name, device.Trust.String()) + device.Name, trustState) } -func cmdVerify(cmd *Command) { +func crossSignDevice(cmd *Command, device *crypto.DeviceIdentity) { + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + err := mach.SignOwnDevice(device) + if err != nil { + cmd.Reply("Failed to upload cross-signing signature: %v", err) + } else { + cmd.Reply("Successfully cross-signed %s (%s)", device.DeviceID, device.Name) + } +} + +func cmdVerifyDevice(cmd *Command) { device := getDevice(cmd) if device == nil { return @@ -188,11 +212,49 @@ func cmdVerify(cmd *Command) { if device.Trust == crypto.TrustStateBlacklisted { action = "unblacklisted and verified" } - device.Trust = crypto.TrustStateVerified - putDevice(cmd, device, action) + if device.UserID == cmd.Matrix.Client().UserID { + crossSignDevice(cmd, device) + device.Trust = crypto.TrustStateVerified + putDevice(cmd, device, action) + } else { + putDevice(cmd, device, action) + cmd.Reply("Warning: verifying individual devices of other users is not synced with cross-signing") + } } } +func cmdVerify(cmd *Command) { + if len(cmd.Args) < 1 { + cmd.Reply("Usage: /%s [--force]", cmd.OrigCommand) + return + } + force := len(cmd.Args) >= 2 && strings.ToLower(cmd.Args[1]) == "--force" + userID := id.UserID(cmd.Args[0]) + room := cmd.Room.Room + if !room.Encrypted { + cmd.Reply("In-room verification is only supported in encrypted rooms") + return + } + if (!room.IsDirect || room.OtherUser != userID) && !force { + cmd.Reply("This doesn't seem to be a direct chat. Either switch to a direct chat with %s, "+ + "or use `--force` to start the verification anyway.", userID) + return + } + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + if mach.CrossSigningKeys == nil && !force { + cmd.Reply("Cross-signing private keys not cached. Generate or fetch cross-signing keys with `/cross-signing`, " + + "or use `--force` to start the verification anyway") + return + } + modal := NewVerificationModal(cmd.MainView, &crypto.DeviceIdentity{UserID: userID}, mach.DefaultSASTimeout) + _, err := mach.NewInRoomSASVerificationWith(cmd.Room.Room.ID, userID, modal, 120*time.Second) + if err != nil { + cmd.Reply("Failed to start in-room verification: %v", err) + return + } + cmd.MainView.ShowModal(modal) +} + func cmdUnverify(cmd *Command) { device := getDevice(cmd) if device == nil { @@ -444,7 +506,7 @@ func cmdCrossSigning(cmd *Command) { switch strings.ToLower(cmd.Args[0]) { case "status": - cmdCrossSigningStatus(cmd, mach, client) + cmdCrossSigningStatus(cmd, mach) case "generate": force := len(cmd.Args) > 1 && strings.ToLower(cmd.Args[1]) == "--force" cmdCrossSigningGenerate(cmd, cmd.Matrix, mach, client, force) @@ -460,48 +522,24 @@ func cmdCrossSigning(cmd *Command) { } } -func parseKeyResp(keys *mautrix.RespQueryKeys, userID id.UserID) (id.Ed25519, id.Ed25519, id.Ed25519, bool) { - masterKeys, ok := keys.MasterKeys[userID] - if !ok { - return "", "", "", false +func cmdCrossSigningStatus(cmd *Command, mach *crypto.OlmMachine) { + keys := mach.GetOwnCrossSigningPublicKeys() + if keys == nil { + if mach.CrossSigningKeys != nil { + cmd.Reply("Cross-signing keys are cached, but not published") + } else { + cmd.Reply("Didn't find published cross-signing keys") + } + return } - selfSigningKeys, ok := keys.SelfSigningKeys[userID] - if !ok { - return "", "", "", false - } - userSigningKeys, ok := keys.UserSigningKeys[userID] - if !ok { - return masterKeys.FirstKey(), selfSigningKeys.FirstKey(), "", true - } - return masterKeys.FirstKey(), userSigningKeys.FirstKey(), selfSigningKeys.FirstKey(), true -} - -func cmdCrossSigningStatus(cmd *Command, mach *crypto.OlmMachine, client *mautrix.Client) { if mach.CrossSigningKeys != nil { - cmd.Reply("Cross-signing is set up and private keys are cached") - cmd.Reply("Master key: %s", mach.CrossSigningKeys.MasterKey.PublicKey) - cmd.Reply("User signing key: %s", mach.CrossSigningKeys.UserSigningKey.PublicKey) - cmd.Reply("Self-signing key: %s", mach.CrossSigningKeys.SelfSigningKey.PublicKey) - return + cmd.Reply("Cross-signing keys are published and private keys are cached") + } else { + cmd.Reply("Cross-signing keys are published, but private keys are not cached") } - keys, err := client.QueryKeys(&mautrix.ReqQueryKeys{ - DeviceKeys: mautrix.DeviceKeysRequest{ - client.UserID: mautrix.DeviceIDList{client.DeviceID}, - }, - }) - if err != nil { - cmd.Reply("Failed to query own keys: %v", err) - return - } - masterKey, selfSigningKey, userSigningKey, ok := parseKeyResp(keys, client.UserID) - if !ok { - cmd.Reply("Didn't find published cross-signing keys") - return - } - cmd.Reply("Cross-signing is set up, but private keys are not cached") - cmd.Reply("Master key: %s", masterKey) - cmd.Reply("User signing key: %s", userSigningKey) - cmd.Reply("Self-signing key: %s", selfSigningKey) + cmd.Reply("Master key: %s", keys.MasterKey) + cmd.Reply("User signing key: %s", keys.UserSigningKey) + cmd.Reply("Self-signing key: %s", keys.SelfSigningKey) } func cmdCrossSigningFetch(cmd *Command, mach *crypto.OlmMachine, saveToDisk bool) { @@ -523,17 +561,10 @@ func cmdCrossSigningFetch(cmd *Command, mach *crypto.OlmMachine, saveToDisk bool func cmdCrossSigningGenerate(cmd *Command, container ifc.MatrixContainer, mach *crypto.OlmMachine, client *mautrix.Client, force bool) { if !force { - keys, err := client.QueryKeys(&mautrix.ReqQueryKeys{ - DeviceKeys: mautrix.DeviceKeysRequest{ - client.UserID: mautrix.DeviceIDList{client.DeviceID}, - }, - }) - if err == nil { - _, _, _, ok := parseKeyResp(keys, client.UserID) - if ok { - cmd.Reply("Found existing cross-signing keys. Use `--force` if you want to overwrite them.") - return - } + existingKeys := mach.GetOwnCrossSigningPublicKeys() + if existingKeys != nil { + cmd.Reply("Found existing cross-signing keys. Use `--force` if you want to overwrite them.") + return } } @@ -557,7 +588,7 @@ func cmdCrossSigningGenerate(cmd *Command, container ifc.MatrixContainer, mach * } return &mautrix.ReqUIAuthFallback{ Session: uia.Session, - User: mach.Client.UserID.String(), + User: mach.Client.UserID.String(), } } cmd.Reply("No supported authentication mechanisms found") @@ -580,8 +611,6 @@ func cmdCrossSigningGenerate(cmd *Command, container ifc.MatrixContainer, mach * cmd.Reply("Failed to publish cross-signing keys: %v", err) return } - - mach.CrossSigningKeys = keys cmd.Reply("Successfully generated and published cross-signing keys") err = mach.SignOwnMasterKey() diff --git a/ui/no-crypto-commands.go b/ui/no-crypto-commands.go index 2d1529d..3669d5a 100644 --- a/ui/no-crypto-commands.go +++ b/ui/no-crypto-commands.go @@ -29,6 +29,7 @@ func cmdNoCrypto(cmd *Command) { var ( cmdDevices = cmdNoCrypto cmdDevice = cmdNoCrypto + cmdVerifyDevice = cmdNoCrypto cmdVerify = cmdNoCrypto cmdUnverify = cmdNoCrypto cmdBlacklist = cmdNoCrypto diff --git a/ui/verification-modal.go b/ui/verification-modal.go index bc529c9..aabc36a 100644 --- a/ui/verification-modal.go +++ b/ui/verification-modal.go @@ -148,7 +148,8 @@ func (vm *VerificationModal) VerificationMethods() []crypto.VerificationMethod { return []crypto.VerificationMethod{crypto.VerificationMethodEmoji{}, crypto.VerificationMethodDecimal{}} } -func (vm *VerificationModal) VerifySASMatch(_ *crypto.DeviceIdentity, data crypto.SASData) bool { +func (vm *VerificationModal) VerifySASMatch(device *crypto.DeviceIdentity, data crypto.SASData) bool { + vm.device = device var typeName string if data.Type() == event.SASDecimal { typeName = "numbers"