From 77a1514c900a0d422a616a548496a48cf6baf35f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 24 Jul 2020 21:47:22 +0300 Subject: Add device list and legacy verification commands --- matrix/matrix.go | 6 +- matrix/rooms/roomcache.go | 13 ++++ ui/command-processor.go | 59 ++++++++++++++ ui/commands.go | 195 +++++++++++++++++++++++++++++++++++++++++++++- ui/room-view.go | 188 ++++++++++++++++++++++++++------------------ ui/view-main.go | 18 +---- 6 files changed, 385 insertions(+), 94 deletions(-) diff --git a/matrix/matrix.go b/matrix/matrix.go index b07fa82..f7f308b 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -162,6 +162,8 @@ func (c *Container) PasswordLogin(user, password string) error { }, Password: password, InitialDeviceDisplayName: "gomuks", + + StoreCredentials: true, }) if err != nil { return err @@ -171,8 +173,6 @@ func (c *Container) PasswordLogin(user, password string) error { } func (c *Container) finishLogin(resp *mautrix.RespLogin) { - c.client.SetCredentials(resp.UserID, resp.AccessToken) - c.client.DeviceID = resp.DeviceID c.config.UserID = resp.UserID c.config.DeviceID = resp.DeviceID c.config.AccessToken = resp.AccessToken @@ -218,6 +218,8 @@ func (c *Container) SingleSignOn() error { Type: "m.login.token", Token: loginToken, InitialDeviceDisplayName: "gomuks", + + StoreCredentials: true, }) if err != nil { respondHTML(w, http.StatusForbidden, err.Error()) diff --git a/matrix/rooms/roomcache.go b/matrix/rooms/roomcache.go index 5af0c5b..168278b 100644 --- a/matrix/rooms/roomcache.go +++ b/matrix/rooms/roomcache.go @@ -73,6 +73,19 @@ func (cache *RoomCache) IsEncrypted(roomID id.RoomID) bool { return room != nil && room.Encrypted } +func (cache *RoomCache) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent { + room := cache.Get(roomID) + evt := room.GetStateEvent(event.StateEncryption, "") + if evt == nil { + return nil + } + content, ok := evt.Content.Parsed.(*event.EncryptionEventContent) + if !ok { + return nil + } + return content +} + func (cache *RoomCache) FindSharedRooms(userID id.UserID) (shared []id.RoomID) { // FIXME this disables unloading so TouchNode wouldn't try to double-lock cache.DisableUnloading() diff --git a/ui/command-processor.go b/ui/command-processor.go index b8f41a2..1b30cbe 100644 --- a/ui/command-processor.go +++ b/ui/command-processor.go @@ -20,6 +20,8 @@ import ( "fmt" "strings" + "github.com/mattn/go-runewidth" + "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" @@ -45,6 +47,8 @@ type Command struct { OrigText string } +type CommandAutocomplete Command + func (cmd *Command) Reply(message string, args ...interface{}) { cmd.Room.AddServiceMessage(fmt.Sprintf(message, args...)) cmd.UI.Render() @@ -60,12 +64,15 @@ func (alias *Alias) Process(cmd *Command) *Command { } type CommandHandler func(cmd *Command) +type CommandAutocompleter func(cmd *CommandAutocomplete) (completions []string, newText string) type CommandProcessor struct { gomuksPointerContainer aliases map[string]*Alias commands map[string]CommandHandler + + autocompleters map[string]CommandAutocompleter } func NewCommandProcessor(parent *MainView) *CommandProcessor { @@ -97,6 +104,12 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "dl": {"download"}, "o": {"open"}, }, + autocompleters: map[string]CommandAutocompleter{ + "devices": autocompleteDevice, + "device": autocompleteDevice, + "verify": autocompleteDevice, + "unverify": autocompleteDevice, + }, commands: map[string]CommandHandler{ "unknown-command": cmdUnknownCommand, @@ -140,6 +153,11 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "trace": cmdTrace, "fingerprint": cmdFingerprint, + "devices": cmdDevices, + "verify": cmdVerify, + "device": cmdDevice, + "unverify": cmdUnverify, + "blacklist": cmdBlacklist, }, } } @@ -169,6 +187,47 @@ func (ch *CommandProcessor) ParseCommand(roomView *RoomView, text string) *Comma } } +func (ch *CommandProcessor) Autocomplete(roomView *RoomView, text string, cursorOffset int) ([]string, string, bool) { + var completions []string + if cmd := (*CommandAutocomplete)(ch.ParseCommand(roomView, text)); cmd == nil { + return completions, text, false + } else if handler, ok := ch.autocompleters[cmd.Command]; !ok { + return completions, text, false + } else if cursorOffset != runewidth.StringWidth(text) { + return completions, text, false + } else { + completions, newText := handler(cmd) + if newText == "" { + newText = text + } + return completions, newText, true + } +} + +func (ch *CommandProcessor) AutocompleteCommand(word string) (completions []string) { + if word[0] != '/' { + return + } + word = word[1:] + for alias := range ch.aliases { + if alias == word { + return []string{"/" + alias} + } + if strings.HasPrefix(alias, word) { + completions = append(completions, "/"+alias) + } + } + for command := range ch.commands { + if command == word { + return []string{"/" + command} + } + if strings.HasPrefix(command, word) { + completions = append(completions, "/"+command) + } + } + return +} + func (ch *CommandProcessor) HandleCommand(cmd *Command) { defer debug.Recover() if cmd == nil { diff --git a/ui/commands.go b/ui/commands.go index 49bff0f..b406278 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -37,6 +37,7 @@ import ( "github.com/russross/blackfriday/v2" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" @@ -365,6 +366,187 @@ func cmdFingerprint(cmd *Command) { } } +// region TODO these four functions currently use the crypto internals directly. switch to interfaces before releasing + +func autocompleteDeviceUserID(cmd *CommandAutocomplete) (completions []string, newText string) { + userCompletions := cmd.Room.AutocompleteUser(cmd.Args[0]) + if len(userCompletions) == 1 { + newText = fmt.Sprintf("/%s %s ", cmd.OrigCommand, userCompletions[0].id) + } else { + completions = make([]string, len(userCompletions)) + for i, completion := range userCompletions { + completions[i] = completion.id + } + } + return +} + +func autocompleteDeviceDeviceID(cmd *CommandAutocomplete) (completions []string, newText string) { + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + devices, err := mach.CryptoStore.GetDevices(id.UserID(cmd.Args[0])) + if len(devices) == 0 || err != nil { + return + } + var completedDeviceID id.DeviceID + if len(cmd.Args) > 1 { + existingID := strings.ToUpper(cmd.Args[1]) + for _, device := range devices { + deviceIDStr := string(device.DeviceID) + if deviceIDStr == existingID { + // We don't want to do any autocompletion if there's already a full device ID there. + return []string{}, "" + } else if strings.HasPrefix(strings.ToUpper(device.Name), existingID) || strings.HasPrefix(deviceIDStr, existingID) { + completedDeviceID = device.DeviceID + completions = append(completions, fmt.Sprintf("%s (%s)", device.DeviceID, device.Name)) + } + } + } else { + completions = make([]string, len(devices)) + i := 0 + for _, device := range devices { + completedDeviceID = device.DeviceID + completions[i] = fmt.Sprintf("%s (%s)", device.DeviceID, device.Name) + i++ + } + } + if len(completions) == 1 { + newText = fmt.Sprintf("/%s %s %s ", cmd.OrigCommand, cmd.Args[0], completedDeviceID) + } + return +} + +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{}, "" +} + +func getDevice(cmd *Command) *crypto.DeviceIdentity { + if len(cmd.Args) < 2 { + cmd.Reply("Usage: /%s [fingerprint]", cmd.Command) + return nil + } + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + device, err := mach.GetOrFetchDevice(id.UserID(cmd.Args[0]), id.DeviceID(cmd.Args[1])) + if err != nil { + cmd.Reply("Failed to get device: %v", err) + return nil + } + return device +} + +func putDevice(cmd *Command, device *crypto.DeviceIdentity, action string) { + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + err := mach.CryptoStore.PutDevice(device.UserID, device) + if err != nil { + cmd.Reply("Failed to save device: %v", err) + } else { + cmd.Reply("Successfully %s %s/%s (%s)", action, device.UserID, device.DeviceID, device.Name) + } + mach.OnDevicesChanged(device.UserID) +} + +func cmdDevices(cmd *Command) { + if len(cmd.Args) == 0 { + cmd.Reply("Usage: /devices ") + return + } + userID := id.UserID(cmd.Args[0]) + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + devices, err := mach.CryptoStore.GetDevices(userID) + if err != nil { + cmd.Reply("Failed to get device list: %v", err) + } + if len(devices) == 0 { + cmd.Reply("Fetching device list from server...") + devices = mach.LoadDevices(userID) + } + if len(devices) == 0 { + cmd.Reply("No devices found for %s", userID) + return + } + var buf strings.Builder + for _, device := range devices { + _, _ = fmt.Fprintf(&buf, "%s (%s) - %s - %s\n", device.DeviceID, device.Name, device.Trust.String(), device.Fingerprint()) + } + resp := buf.String() + cmd.Reply(resp[:len(resp)-1]) +} + +func cmdDevice(cmd *Command) { + device := getDevice(cmd) + if device == nil { + return + } + deviceType := "Device" + if device.Deleted { + deviceType = "Deleted device" + } + 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()) +} + +func cmdVerify(cmd *Command) { + device := getDevice(cmd) + if device == nil { + return + } + if len(cmd.Args) == 2 { + cmd.Reply("Interactive verification UI is not yet implemented") + } else { + fingerprint := strings.Join(cmd.Args[2:], "") + if string(device.SigningKey) != fingerprint { + cmd.Reply("Mismatching fingerprint") + return + } + action := "verified" + if device.Trust == crypto.TrustStateBlacklisted { + action = "unblacklisted and verified" + } + device.Trust = crypto.TrustStateVerified + putDevice(cmd, device, action) + } +} + +func cmdUnverify(cmd *Command) { + device := getDevice(cmd) + if device == nil { + return + } + if device.Trust == crypto.TrustStateUnset { + cmd.Reply("That device is already not verified") + return + } + action := "unverified" + if device.Trust == crypto.TrustStateBlacklisted { + action = "unblacklisted" + } + device.Trust = crypto.TrustStateUnset + putDevice(cmd, device, action) +} + +func cmdBlacklist(cmd *Command) { + device := getDevice(cmd) + if device == nil { + return + } + action := "blacklisted" + if device.Trust == crypto.TrustStateVerified { + action = "unverified and blacklisted" + } + device.Trust = crypto.TrustStateBlacklisted + putDevice(cmd, device, action) +} + +// endregion + func cmdHeapProfile(cmd *Command) { if len(cmd.Args) == 0 || cmd.Args[0] != "nogc" { runtime.GC() @@ -449,7 +631,18 @@ Things: rooms, users, baremessages, images, typingnotif /rainbowme - Send rainbow text in an emote. /reply [text] - Reply to the selected message. /react - React to the selected message. -/redact [reason] - Redact the selected message. +/redact [reason] - Redact the selected message. + +# Encryption +/fingerprint - View the fingerprint of your device. + +/devices - View the device list of a user. +/device - Show info about a specific device. +/unverify - Un-verify a device. +/blacklist - Blacklist a device. +/verify [fingerprint] + - Verify a device. If the fingerprint is not provided, + interactive emoji verification will be started. # Rooms /pm <...> - Create a private chat with the given user(s). diff --git a/ui/room-view.go b/ui/room-view.go index dbdaccc..38a561c 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -22,15 +22,18 @@ import ( "sort" "strings" "time" + "unicode" "github.com/kyokomi/emoji" "github.com/mattn/go-runewidth" "github.com/zyedidia/clipboard" - "maunium.net/go/mautrix/crypto/attachment" "maunium.net/go/mauview" "maunium.net/go/tcell" + "maunium.net/go/gomuks/lib/util" + "maunium.net/go/mautrix/crypto/attachment" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -39,7 +42,6 @@ import ( "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/lib/open" - "maunium.net/go/gomuks/lib/util" "maunium.net/go/gomuks/matrix/muksevt" "maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/ui/messages" @@ -420,65 +422,6 @@ func (view *RoomView) SetTyping(users []id.UserID) { } } -type completion struct { - displayName string - id string -} - -func (view *RoomView) autocompleteUser(existingText string) (completions []completion) { - textWithoutPrefix := strings.TrimPrefix(existingText, "@") - for userID, user := range view.Room.GetMembers() { - if user.Displayname == textWithoutPrefix || string(userID) == existingText { - // Exact match, return that. - return []completion{{user.Displayname, string(userID)}} - } - - if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) { - completions = append(completions, completion{user.Displayname, string(userID)}) - } - } - return -} - -func (view *RoomView) autocompleteRoom(existingText string) (completions []completion) { - for _, room := range view.parent.rooms { - alias := string(room.Room.GetCanonicalAlias()) - if alias == existingText { - // Exact match, return that. - return []completion{{alias, string(room.Room.ID)}} - } - if strings.HasPrefix(alias, existingText) { - completions = append(completions, completion{alias, string(room.Room.ID)}) - continue - } - } - return -} - -func (view *RoomView) autocompleteEmoji(word string) (completions []string) { - if len(word) == 0 || word[0] != ':' { - return - } - var valueCompletion1 string - var manyValues bool - for name, value := range emoji.CodeMap() { - if name == word { - return []string{value} - } else if strings.HasPrefix(name, word) { - completions = append(completions, name) - if valueCompletion1 == "" { - valueCompletion1 = value - } else if valueCompletion1 != value { - manyValues = true - } - } - } - if !manyValues && len(completions) > 0 { - return []string{emoji.CodeMap()[completions[0]]} - } - return -} - func (view *RoomView) SetEditing(evt *muksevt.Event) { if evt == nil { view.editing = nil @@ -584,23 +527,90 @@ func (view *RoomView) SelectPrevious() { } } -func (view *RoomView) InputTabComplete(text string, cursorOffset int) { - debug.Print("Tab completing", cursorOffset, text) - str := runewidth.Truncate(text, cursorOffset, "") - word := findWordToTabComplete(str) - startIndex := len(str) - len(word) +type completion struct { + displayName string + id string +} - var strCompletions []string - var strCompletion string +func (view *RoomView) AutocompleteUser(existingText string) (completions []completion) { + textWithoutPrefix := strings.TrimPrefix(existingText, "@") + for userID, user := range view.Room.GetMembers() { + if user.Displayname == textWithoutPrefix || string(userID) == existingText { + // Exact match, return that. + return []completion{{user.Displayname, string(userID)}} + } - completions := view.autocompleteUser(word) - completions = append(completions, view.autocompleteRoom(word)...) + if strings.HasPrefix(user.Displayname, textWithoutPrefix) || strings.HasPrefix(string(userID), existingText) { + completions = append(completions, completion{user.Displayname, string(userID)}) + } + } + return +} + +func (view *RoomView) AutocompleteRoom(existingText string) (completions []completion) { + for _, room := range view.parent.rooms { + alias := string(room.Room.GetCanonicalAlias()) + if alias == existingText { + // Exact match, return that. + return []completion{{alias, string(room.Room.ID)}} + } + if strings.HasPrefix(alias, existingText) { + completions = append(completions, completion{alias, string(room.Room.ID)}) + continue + } + } + return +} + +func (view *RoomView) AutocompleteEmoji(word string) (completions []string) { + if word[0] != ':' { + return + } + var valueCompletion1 string + var manyValues bool + for name, value := range emoji.CodeMap() { + if name == word { + return []string{value} + } else if strings.HasPrefix(name, word) { + completions = append(completions, name) + if valueCompletion1 == "" { + valueCompletion1 = value + } else if valueCompletion1 != value { + manyValues = true + } + } + } + if !manyValues && len(completions) > 0 { + return []string{emoji.CodeMap()[completions[0]]} + } + return +} + +func findWordToTabComplete(text string) string { + output := "" + runes := []rune(text) + for i := len(runes) - 1; i >= 0; i-- { + if unicode.IsSpace(runes[i]) { + break + } + output = string(runes[i]) + output + } + return output +} + +func (view *RoomView) defaultAutocomplete(word string, startIndex int) (strCompletions []string, strCompletion string) { + if len(word) == 0 { + return []string{}, "" + } + + completions := view.AutocompleteUser(word) + completions = append(completions, view.AutocompleteRoom(word)...) if len(completions) == 1 { completion := completions[0] strCompletion = fmt.Sprintf("[%s](https://matrix.to/#/%s)", completion.displayName, completion.id) - if startIndex == 0 { - strCompletion = strCompletion + ": " + if startIndex == 0 && completion.id[0] == '@' { + strCompletion = strCompletion + ":" } } else if len(completions) > 1 { for _, completion := range completions { @@ -608,18 +618,44 @@ func (view *RoomView) InputTabComplete(text string, cursorOffset int) { } } - strCompletions = append(strCompletions, view.autocompleteEmoji(word)...) + strCompletions = append(strCompletions, view.parent.cmdProcessor.AutocompleteCommand(word)...) + strCompletions = append(strCompletions, view.AutocompleteEmoji(word)...) + + return +} + +func (view *RoomView) InputTabComplete(text string, cursorOffset int) { + if len(text) == 0 { + return + } + + debug.Print("Tab completing", cursorOffset, text) + + str := runewidth.Truncate(text, cursorOffset, "") + word := findWordToTabComplete(str) + startIndex := len(str) - len(word) + + var strCompletion string + + strCompletions, newText, ok := view.parent.cmdProcessor.Autocomplete(view, text, cursorOffset) + if !ok { + strCompletions, strCompletion = view.defaultAutocomplete(word, startIndex) + } if len(strCompletions) > 0 { strCompletion = util.LongestCommonPrefix(strCompletions) sort.Sort(sort.StringSlice(strCompletions)) } + if len(strCompletion) > 0 && len(strCompletions) < 2 { + strCompletion += " " + strCompletions = []string{} + } - if len(strCompletion) > 0 { - text = str[0:startIndex] + strCompletion + text[len(str):] + if len(strCompletion) > 0 && newText == text { + newText = str[0:startIndex] + strCompletion + text[len(str):] } - view.input.SetTextAndMoveCursor(text) + view.input.SetTextAndMoveCursor(newText) view.SetCompletions(strCompletions) } diff --git a/ui/view-main.go b/ui/view-main.go index 0f6098c..fc1b9c9 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -22,15 +22,15 @@ import ( "os" "sync/atomic" "time" - "unicode" sync "github.com/sasha-s/go-deadlock" - "maunium.net/go/gomuks/ui/messages" - "maunium.net/go/mautrix/id" "maunium.net/go/mauview" "maunium.net/go/tcell" + "maunium.net/go/gomuks/ui/messages" + "maunium.net/go/mautrix/id" + "maunium.net/go/gomuks/config" "maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/interface" @@ -139,18 +139,6 @@ func (view *MainView) InputChanged(roomView *RoomView, text string) { } } -func findWordToTabComplete(text string) string { - output := "" - runes := []rune(text) - for i := len(runes) - 1; i >= 0; i-- { - if unicode.IsSpace(runes[i]) { - break - } - output = string(runes[i]) + output - } - return output -} - func (view *MainView) ShowBare(roomView *RoomView) { if roomView == nil { return -- cgit v1.2.3 From 341f8829d67b197ece20fe1cb0da929486665853 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 24 Jul 2020 23:44:04 +0300 Subject: Add very crude interactive verification support --- ui/commands.go | 14 +++- ui/verification-modal.go | 192 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 ui/verification-modal.go diff --git a/ui/commands.go b/ui/commands.go index b406278..a10445f 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -472,7 +472,7 @@ func cmdDevices(cmd *Command) { } var buf strings.Builder for _, device := range devices { - _, _ = fmt.Fprintf(&buf, "%s (%s) - %s - %s\n", device.DeviceID, device.Name, device.Trust.String(), device.Fingerprint()) + _, _ = fmt.Fprintf(&buf, "%s (%s) - %s\n Fingerprint: %s\n", device.DeviceID, device.Name, device.Trust.String(), device.Fingerprint()) } resp := buf.String() cmd.Reply(resp[:len(resp)-1]) @@ -499,7 +499,17 @@ func cmdVerify(cmd *Command) { return } if len(cmd.Args) == 2 { - cmd.Reply("Interactive verification UI is not yet implemented") + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + timeout := 60 * time.Second + err := mach.NewSASVerificationWith(device, "", timeout, true) + if err != nil { + cmd.Reply("Failed to start interactive verification: %v", err) + return + } + modal := NewVerificationModal(cmd.MainView, device, timeout) + mach.VerifySASEmojisMatch = modal.VerifyEmojisMatch + mach.VerifySASNumbersMatch = modal.VerifyNumbersMatch + cmd.MainView.ShowModal(modal) } else { fingerprint := strings.Join(cmd.Args[2:], "") if string(device.SigningKey) != fingerprint { diff --git a/ui/verification-modal.go b/ui/verification-modal.go new file mode 100644 index 0000000..fd77777 --- /dev/null +++ b/ui/verification-modal.go @@ -0,0 +1,192 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2020 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ui + +import ( + "fmt" + "strconv" + "strings" + "time" + + "maunium.net/go/mauview" + "maunium.net/go/tcell" + + "maunium.net/go/gomuks/debug" + "maunium.net/go/mautrix/crypto" +) + +type EmojiView struct { + mauview.SimpleEventHandler + Numbers *[3]uint + Emojis *[7]crypto.VerificationEmoji +} + +func (e *EmojiView) Draw(screen mauview.Screen) { + if e.Emojis != nil { + width := 10 + for i, emoji := range e.Emojis { + x := i*width + i + y := 0 + if i >= 4 { + x = (i-4)*width + i + y = 2 + } + mauview.Print(screen, string(emoji.Emoji), x, y, width, mauview.AlignCenter, tcell.ColorDefault) + mauview.Print(screen, emoji.Description, x, y+1, width, mauview.AlignCenter, tcell.ColorDefault) + } + } else if e.Numbers != nil { + maxWidth := 43 + for i, number := range e.Numbers { + mauview.Print(screen, strconv.FormatUint(uint64(number), 10), 0, i, maxWidth, mauview.AlignCenter, tcell.ColorDefault) + } + } +} + +type VerificationModal struct { + mauview.Component + + container *mauview.Box + + waitingBar *mauview.ProgressBar + infoText *mauview.TextView + emojiText *EmojiView + inputBar *mauview.InputField + + stopWaiting chan struct{} + confirmChan chan bool + + parent *MainView +} + +func NewVerificationModal(mainView *MainView, device *crypto.DeviceIdentity, timeout time.Duration) *VerificationModal { + vm := &VerificationModal{ + parent: mainView, + stopWaiting: make(chan struct{}), + confirmChan: make(chan bool), + } + + progress := int(timeout.Seconds()) + vm.waitingBar = mauview.NewProgressBar(). + SetMax(progress). + SetProgress(progress). + SetIndeterminate(false) + + vm.infoText = mauview.NewTextView() + vm.infoText.SetText(fmt.Sprintf("Waiting for %s to accept", device.UserID)) + + vm.emojiText = &EmojiView{} + + vm.inputBar = mauview.NewInputField().SetBackgroundColor(tcell.ColorDefault) + + flex := mauview.NewFlex(). + SetDirection(mauview.FlexRow). + AddFixedComponent(vm.waitingBar, 1). + AddFixedComponent(vm.infoText, 4). + AddFixedComponent(vm.emojiText, 4). + AddFixedComponent(vm.inputBar, 1) + + vm.container = mauview.NewBox(flex). + SetBorder(true). + SetTitle("Interactive verification") + + vm.Component = mauview.Center(vm.container, 45, 12).SetAlwaysFocusChild(true) + + go vm.decrementWaitingBar(progress) + + return vm +} + +func (vm *VerificationModal) decrementWaitingBar(progress int) { + for { + select { + case <-time.Tick(time.Second): + if progress <= 0 { + vm.parent.HideModal() + vm.parent.parent.Render() + return + } + progress-- + vm.waitingBar.SetProgress(progress) + vm.parent.parent.Render() + case <-vm.stopWaiting: + vm.waitingBar.SetIndeterminate(true) + break + } + } +} + +func (vm *VerificationModal) VerifyEmojisMatch(emojis [7]crypto.VerificationEmoji, _ *crypto.DeviceIdentity) bool { + vm.infoText.SetText("Check if the other device is showing the same emojis as below, then type \"yes\" to accept, or \"no\" to reject") + vm.inputBar. + SetTextColor(tcell.ColorWhite). + SetBackgroundColor(tcell.ColorDarkCyan). + SetPlaceholder("Type \"yes\" or \"no\""). + Focus() + vm.emojiText.Emojis = &emojis + vm.parent.parent.Render() + vm.stopWaiting <- struct{}{} + confirm := <-vm.confirmChan + // TODO this should hook into cancel/success of the verification and display a success message instead of just closing + vm.parent.HideModal() + vm.parent.parent.Render() + return confirm +} + +func (vm *VerificationModal) VerifyNumbersMatch(numbers [3]uint, _ *crypto.DeviceIdentity) bool { + vm.infoText.SetText("Check if the other device is showing the same numbers as below, then type \"yes\" to accept, or \"no\" to reject") + vm.inputBar. + SetTextColor(tcell.ColorWhite). + SetBackgroundColor(tcell.ColorDarkCyan). + SetPlaceholder("Type \"yes\" or \"no\""). + Focus() + vm.emojiText.Numbers = &numbers + vm.parent.parent.Render() + vm.stopWaiting <- struct{}{} + confirm := <-vm.confirmChan + // TODO this should hook into cancel/success of the verification and display a success message instead of just closing + vm.parent.HideModal() + vm.parent.parent.Render() + return confirm +} + +func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { + if vm.emojiText.Emojis == nil && vm.emojiText.Numbers == nil { + debug.Print("Ignoring pre-emoji key event") + return false + } + if event.Key() == tcell.KeyEnter { + text := strings.ToLower(strings.TrimSpace(vm.inputBar.GetText())) + if text == "yes" { + debug.Print("Confirming verification") + vm.confirmChan <- true + } else if text == "no" { + debug.Print("Rejecting verification") + vm.confirmChan <- false + } + return true + } else { + return vm.inputBar.OnKeyEvent(event) + } +} + +func (vm *VerificationModal) Focus() { + vm.container.Focus() +} + +func (vm *VerificationModal) Blur() { + vm.container.Blur() +} -- cgit v1.2.3 From ead7e0bf1d9c584224c1738b32ad26e314957220 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 25 Jul 2020 18:40:31 +0300 Subject: Make verification modal wait for confirmation --- go.mod | 2 ++ go.sum | 8 +++++++ ui/commands.go | 18 ++++++++++----- ui/verification-modal.go | 58 ++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 38c02d9..9b50058 100644 --- a/go.mod +++ b/go.mod @@ -27,3 +27,5 @@ require ( maunium.net/go/mauview v0.1.1 maunium.net/go/tcell v0.2.0 ) + +replace maunium.net/go/mautrix => github.com/nikofil/mautrix-go v0.5.2-0.20200725145808-be3336827da7 diff --git a/go.sum b/go.sum index e4ebe5d..33fe477 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,11 @@ +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI= github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -28,6 +30,9 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/nikofil/mautrix-go v0.5.2-0.20200725145808-be3336827da7 h1:w5IiIpetgAwalLPFkxiVbxBXJtcB9zLhoGLkyouQb4k= +github.com/nikofil/mautrix-go v0.5.2-0.20200725145808-be3336827da7/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= @@ -70,7 +75,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/ui/commands.go b/ui/commands.go index a10445f..a6309fd 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -498,18 +498,20 @@ func cmdVerify(cmd *Command) { if device == nil { return } + if device.Trust == crypto.TrustStateVerified { + cmd.Reply("That device is already verified") + return + } if len(cmd.Args) == 2 { mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) - timeout := 60 * time.Second - err := mach.NewSASVerificationWith(device, "", timeout, true) + mach.DefaultSASTimeout = 120 * time.Second + modal := NewVerificationModal(cmd.MainView, device, mach.DefaultSASTimeout) + cmd.MainView.ShowModal(modal) + err := mach.NewSimpleSASVerificationWith(device, modal) if err != nil { cmd.Reply("Failed to start interactive verification: %v", err) return } - modal := NewVerificationModal(cmd.MainView, device, timeout) - mach.VerifySASEmojisMatch = modal.VerifyEmojisMatch - mach.VerifySASNumbersMatch = modal.VerifyNumbersMatch - cmd.MainView.ShowModal(modal) } else { fingerprint := strings.Join(cmd.Args[2:], "") if string(device.SigningKey) != fingerprint { @@ -547,6 +549,10 @@ func cmdBlacklist(cmd *Command) { if device == nil { return } + if device.Trust == crypto.TrustStateBlacklisted { + cmd.Reply("That device is already blacklisted") + return + } action := "blacklisted" if device.Trust == crypto.TrustStateVerified { action = "unverified and blacklisted" diff --git a/ui/verification-modal.go b/ui/verification-modal.go index fd77777..e47006e 100644 --- a/ui/verification-modal.go +++ b/ui/verification-modal.go @@ -27,6 +27,7 @@ import ( "maunium.net/go/gomuks/debug" "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/event" ) type EmojiView struct { @@ -59,6 +60,8 @@ func (e *EmojiView) Draw(screen mauview.Screen) { type VerificationModal struct { mauview.Component + device *crypto.DeviceIdentity + container *mauview.Box waitingBar *mauview.ProgressBar @@ -68,6 +71,7 @@ type VerificationModal struct { stopWaiting chan struct{} confirmChan chan bool + done bool parent *MainView } @@ -75,8 +79,10 @@ type VerificationModal struct { func NewVerificationModal(mainView *MainView, device *crypto.DeviceIdentity, timeout time.Duration) *VerificationModal { vm := &VerificationModal{ parent: mainView, + device: device, stopWaiting: make(chan struct{}), confirmChan: make(chan bool), + done: false, } progress := int(timeout.Seconds()) @@ -115,8 +121,6 @@ func (vm *VerificationModal) decrementWaitingBar(progress int) { select { case <-time.Tick(time.Second): if progress <= 0 { - vm.parent.HideModal() - vm.parent.parent.Render() return } progress-- @@ -124,13 +128,14 @@ func (vm *VerificationModal) decrementWaitingBar(progress int) { vm.parent.parent.Render() case <-vm.stopWaiting: vm.waitingBar.SetIndeterminate(true) - break + vm.parent.parent.app.SetRedrawTicker(100 * time.Millisecond) + return } } } -func (vm *VerificationModal) VerifyEmojisMatch(emojis [7]crypto.VerificationEmoji, _ *crypto.DeviceIdentity) bool { - vm.infoText.SetText("Check if the other device is showing the same emojis as below, then type \"yes\" to accept, or \"no\" to reject") +func (vm *VerificationModal) VerifyEmojisMatch(emojis [7]crypto.VerificationEmoji) bool { + vm.infoText.SetText("Check if the other device is showing the\nsame emojis as below, then type \"yes\" to\naccept, or \"no\" to reject") vm.inputBar. SetTextColor(tcell.ColorWhite). SetBackgroundColor(tcell.ColorDarkCyan). @@ -140,14 +145,14 @@ func (vm *VerificationModal) VerifyEmojisMatch(emojis [7]crypto.VerificationEmoj vm.parent.parent.Render() vm.stopWaiting <- struct{}{} confirm := <-vm.confirmChan - // TODO this should hook into cancel/success of the verification and display a success message instead of just closing - vm.parent.HideModal() + vm.emojiText.Emojis = nil + vm.infoText.SetText(fmt.Sprintf("Waiting for %s to accept", vm.device.UserID)) vm.parent.parent.Render() return confirm } -func (vm *VerificationModal) VerifyNumbersMatch(numbers [3]uint, _ *crypto.DeviceIdentity) bool { - vm.infoText.SetText("Check if the other device is showing the same numbers as below, then type \"yes\" to accept, or \"no\" to reject") +func (vm *VerificationModal) VerifyNumbersMatch(numbers [3]uint) bool { + vm.infoText.SetText("Check if the other device is showing the\nsame numbers as below, then type \"yes\" to\naccept, or \"no\" to reject") vm.inputBar. SetTextColor(tcell.ColorWhite). SetBackgroundColor(tcell.ColorDarkCyan). @@ -157,14 +162,42 @@ func (vm *VerificationModal) VerifyNumbersMatch(numbers [3]uint, _ *crypto.Devic vm.parent.parent.Render() vm.stopWaiting <- struct{}{} confirm := <-vm.confirmChan - // TODO this should hook into cancel/success of the verification and display a success message instead of just closing - vm.parent.HideModal() + vm.emojiText.Numbers = nil + vm.infoText.SetText(fmt.Sprintf("Waiting for %s to accept", vm.device.UserID)) vm.parent.parent.Render() return confirm } +func (vm *VerificationModal) OnCancel(cancelledByUs bool, reason string, _ event.VerificationCancelCode) { + vm.waitingBar.SetIndeterminate(false).SetMax(100).SetProgress(100) + vm.parent.parent.app.SetRedrawTicker(1 * time.Minute) + if cancelledByUs { + vm.infoText.SetText(fmt.Sprintf("Verification failed: %s", reason)) + } else { + vm.infoText.SetText(fmt.Sprintf("Verification cancelled by %s: %s", vm.device.UserID, reason)) + } + vm.inputBar.SetPlaceholder("Press enter to close dialog") + vm.done = true + vm.parent.parent.Render() +} + +func (vm *VerificationModal) OnSuccess() { + vm.waitingBar.SetIndeterminate(false).SetMax(100).SetProgress(100) + vm.parent.parent.app.SetRedrawTicker(1 * time.Minute) + vm.infoText.SetText(fmt.Sprintf("Successfully verified %s (%s) of %s", vm.device.Name, vm.device.DeviceID, vm.device.UserID)) + vm.inputBar.SetPlaceholder("Press enter to close dialog") + vm.done = true + vm.parent.parent.Render() +} + func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { - if vm.emojiText.Emojis == nil && vm.emojiText.Numbers == nil { + if vm.done { + if event.Key() == tcell.KeyEnter || event.Key() == tcell.KeyEsc { + vm.parent.HideModal() + return true + } + return false + } else if vm.emojiText.Emojis == nil && vm.emojiText.Numbers == nil { debug.Print("Ignoring pre-emoji key event") return false } @@ -177,6 +210,7 @@ func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { debug.Print("Rejecting verification") vm.confirmChan <- false } + vm.inputBar.SetTextAndMoveCursor("") return true } else { return vm.inputBar.OnKeyEvent(event) -- cgit v1.2.3 From ee3594db46fe261962f0a8a11c48cb9d6f84938f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 25 Jul 2020 20:54:32 +0300 Subject: Add toggle to only send to verified devices --- config/config.go | 6 ++++-- go.mod | 2 +- go.sum | 4 ++++ matrix/crypto.go | 4 +++- ui/command-processor.go | 13 +++++++------ ui/commands.go | 16 ++++++++++++++-- ui/verification-modal.go | 12 +++++++++--- 7 files changed, 42 insertions(+), 15 deletions(-) diff --git a/config/config.go b/config/config.go index a958b1c..03e186f 100644 --- a/config/config.go +++ b/config/config.go @@ -62,7 +62,8 @@ type Config struct { RoomCacheSize int `yaml:"room_cache_size"` RoomCacheAge int64 `yaml:"room_cache_age"` - NotifySound bool `yaml:"notify_sound"` + NotifySound bool `yaml:"notify_sound"` + SendToVerifiedOnly bool `yaml:"send_to_verified_only"` Dir string `yaml:"-"` DataDir string `yaml:"data_dir"` @@ -96,7 +97,8 @@ func NewConfig(configDir, dataDir, cacheDir, downloadDir string) *Config { RoomCacheSize: 32, RoomCacheAge: 1 * 60, - NotifySound: true, + NotifySound: true, + SendToVerifiedOnly: false, } } diff --git a/go.mod b/go.mod index 9b50058..3e89c1e 100644 --- a/go.mod +++ b/go.mod @@ -28,4 +28,4 @@ require ( maunium.net/go/tcell v0.2.0 ) -replace maunium.net/go/mautrix => github.com/nikofil/mautrix-go v0.5.2-0.20200725145808-be3336827da7 +replace maunium.net/go/mautrix => github.com/nikofil/mautrix-go v0.5.2-0.20200725175335-dd3f90913c4d diff --git a/go.sum b/go.sum index 33fe477..089c3ab 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,10 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/nikofil/mautrix-go v0.5.2-0.20200725145808-be3336827da7 h1:w5IiIpetgAwalLPFkxiVbxBXJtcB9zLhoGLkyouQb4k= github.com/nikofil/mautrix-go v0.5.2-0.20200725145808-be3336827da7/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= +github.com/nikofil/mautrix-go v0.5.2-0.20200725165923-cae799088b7e h1:K8D+9n29FP1Utrv2joUhjcuDL1zC+Qq9rEFdV5kPgTQ= +github.com/nikofil/mautrix-go v0.5.2-0.20200725165923-cae799088b7e/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= +github.com/nikofil/mautrix-go v0.5.2-0.20200725175335-dd3f90913c4d h1:tsCIy4CvvAeFEoBhet7zGoZXc+lr/e2YogoPaEGdMN8= +github.com/nikofil/mautrix-go v0.5.2-0.20200725175335-dd3f90913c4d/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= diff --git a/matrix/crypto.go b/matrix/crypto.go index 158a1f2..c549e8a 100644 --- a/matrix/crypto.go +++ b/matrix/crypto.go @@ -55,7 +55,9 @@ func (c *Container) initCrypto() error { if err != nil { return errors.Wrap(err, "failed to open crypto store") } - c.crypto = crypto.NewOlmMachine(c.client, cryptoLogger{}, cryptoStore, c.config.Rooms) + crypt := crypto.NewOlmMachine(c.client, cryptoLogger{}, cryptoStore, c.config.Rooms) + crypt.AllowUnverifiedDevices = !c.config.SendToVerifiedOnly + c.crypto = crypt err = c.crypto.Load() if err != nil { return errors.Wrap(err, "failed to create olm machine") diff --git a/ui/command-processor.go b/ui/command-processor.go index 1b30cbe..514d67b 100644 --- a/ui/command-processor.go +++ b/ui/command-processor.go @@ -152,12 +152,13 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor { "cprof": cmdCPUProfile, "trace": cmdTrace, - "fingerprint": cmdFingerprint, - "devices": cmdDevices, - "verify": cmdVerify, - "device": cmdDevice, - "unverify": cmdUnverify, - "blacklist": cmdBlacklist, + "fingerprint": cmdFingerprint, + "devices": cmdDevices, + "verify": cmdVerify, + "device": cmdDevice, + "unverify": cmdUnverify, + "blacklist": cmdBlacklist, + "reset-session": cmdResetSession, }, } } diff --git a/ui/commands.go b/ui/commands.go index a6309fd..1877c89 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -475,7 +475,7 @@ func cmdDevices(cmd *Command) { _, _ = fmt.Fprintf(&buf, "%s (%s) - %s\n Fingerprint: %s\n", device.DeviceID, device.Name, device.Trust.String(), device.Fingerprint()) } resp := buf.String() - cmd.Reply(resp[:len(resp)-1]) + cmd.Reply("%s", resp[:len(resp)-1]) } func cmdDevice(cmd *Command) { @@ -561,6 +561,15 @@ func cmdBlacklist(cmd *Command) { putDevice(cmd, device, action) } +func cmdResetSession(cmd *Command) { + err := cmd.Matrix.Crypto().(*crypto.OlmMachine).CryptoStore.RemoveOutboundGroupSession(cmd.Room.Room.ID) + if err != nil { + cmd.Reply("Failed to remove outbound group session: %v", err) + } else { + cmd.Reply("Removed outbound group session for this room") + } +} + // endregion func cmdHeapProfile(cmd *Command) { @@ -638,7 +647,7 @@ func cmdHelp(cmd *Command) { /logout - Log out of Matrix. /toggle - Temporary command to toggle various UI features. -Things: rooms, users, baremessages, images, typingnotif +Things: rooms, users, baremessages, images, typingnotif, unverified # Sending special messages /me - Send an emote message. @@ -919,6 +928,7 @@ var toggleMsg = map[string]ToggleMessage{ "markdown": SimpleToggleMessage("markdown input"), "downloads": SimpleToggleMessage("automatic downloads"), "notifications": SimpleToggleMessage("desktop notifications"), + "unverified": SimpleToggleMessage("sending messages to unverified devices"), } func makeUsage() string { @@ -959,6 +969,8 @@ func cmdToggle(cmd *Command) { val = &cmd.Config.Preferences.DisableDownloads case "notifications": val = &cmd.Config.Preferences.DisableNotifications + case "unverified": + val = &cmd.Config.SendToVerifiedOnly default: cmd.Reply("Unknown toggle %s. Use /toggle without arguments for a list of togglable things.", thing) return diff --git a/ui/verification-modal.go b/ui/verification-modal.go index e47006e..54c2ecf 100644 --- a/ui/verification-modal.go +++ b/ui/verification-modal.go @@ -96,7 +96,9 @@ func NewVerificationModal(mainView *MainView, device *crypto.DeviceIdentity, tim vm.emojiText = &EmojiView{} - vm.inputBar = mauview.NewInputField().SetBackgroundColor(tcell.ColorDefault) + vm.inputBar = mauview.NewInputField(). + SetBackgroundColor(tcell.ColorDefault). + SetPlaceholderTextColor(tcell.ColorWhite) flex := mauview.NewFlex(). SetDirection(mauview.FlexRow). @@ -176,7 +178,7 @@ func (vm *VerificationModal) OnCancel(cancelledByUs bool, reason string, _ event } else { vm.infoText.SetText(fmt.Sprintf("Verification cancelled by %s: %s", vm.device.UserID, reason)) } - vm.inputBar.SetPlaceholder("Press enter to close dialog") + vm.inputBar.SetPlaceholder("Press enter to close the dialog") vm.done = true vm.parent.parent.Render() } @@ -185,9 +187,13 @@ func (vm *VerificationModal) OnSuccess() { vm.waitingBar.SetIndeterminate(false).SetMax(100).SetProgress(100) vm.parent.parent.app.SetRedrawTicker(1 * time.Minute) vm.infoText.SetText(fmt.Sprintf("Successfully verified %s (%s) of %s", vm.device.Name, vm.device.DeviceID, vm.device.UserID)) - vm.inputBar.SetPlaceholder("Press enter to close dialog") + vm.inputBar.SetPlaceholder("Press enter to close the dialog") vm.done = true vm.parent.parent.Render() + if vm.parent.config.SendToVerifiedOnly { + // Hacky way to make new group sessions after verified + vm.parent.matrix.Crypto().(*crypto.OlmMachine).OnDevicesChanged(vm.device.UserID) + } } func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { -- cgit v1.2.3 From aac9db09d6e2e678dceacc0b43d3e4e4878d9933 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 25 Jul 2020 20:59:54 +0300 Subject: Clear verification modal input bar placeholder after accepting --- ui/verification-modal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/verification-modal.go b/ui/verification-modal.go index 54c2ecf..b3fbd35 100644 --- a/ui/verification-modal.go +++ b/ui/verification-modal.go @@ -216,6 +216,7 @@ func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { debug.Print("Rejecting verification") vm.confirmChan <- false } + vm.inputBar.SetPlaceholder("") vm.inputBar.SetTextAndMoveCursor("") return true } else { -- cgit v1.2.3 From 2f5f0674b600f129204958c810843d998f6a2f6a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 30 Jul 2020 14:32:59 +0300 Subject: Update mautrix-go and make it build without crypto --- go.mod | 4 +- go.sum | 2 + ui/commands.go | 207 ------------------------------------------ ui/crypto-commands.go | 231 +++++++++++++++++++++++++++++++++++++++++++++++ ui/no-crypto-commands.go | 36 ++++++++ ui/room-view.go | 2 - ui/verification-modal.go | 94 ++++++++++--------- 7 files changed, 323 insertions(+), 253 deletions(-) create mode 100644 ui/crypto-commands.go create mode 100644 ui/no-crypto-commands.go diff --git a/go.mod b/go.mod index 3e89c1e..357fb8c 100644 --- a/go.mod +++ b/go.mod @@ -23,9 +23,7 @@ require ( golang.org/x/net v0.0.0-20200602114024-627f9648deb9 gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 gopkg.in/yaml.v2 v2.3.0 - maunium.net/go/mautrix v0.5.5 + maunium.net/go/mautrix v0.7.0-rc.1 maunium.net/go/mauview v0.1.1 maunium.net/go/tcell v0.2.0 ) - -replace maunium.net/go/mautrix => github.com/nikofil/mautrix-go v0.5.2-0.20200725175335-dd3f90913c4d diff --git a/go.sum b/go.sum index 089c3ab..f3bd493 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,8 @@ maunium.net/go/mautrix v0.5.3 h1:Lu4PGZvKZwhXmdDnBdLwmiImNM3jNGwJI9Sk78P+Gys= maunium.net/go/mautrix v0.5.3/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY= maunium.net/go/mautrix v0.5.5 h1:e0Pql1FdxoNUudx2oXo1gZHMrqIh5MC72cdXEPIrYLA= maunium.net/go/mautrix v0.5.5/go.mod h1:FLbMANzwqlsX2Fgm7SDe+E4I3wSa4UxJRKqS5wGkCwA= +maunium.net/go/mautrix v0.7.0-rc.1 h1:DT7bNR9q+HlFs5Oo9IqmtWPkE4WPKZdRfIWRtlqkXtM= +maunium.net/go/mautrix v0.7.0-rc.1/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= maunium.net/go/mauview v0.1.1 h1:wfTXyPx3LGAGpTskh+UbBv/QItUWnEpaneHmywoYnfY= maunium.net/go/mauview v0.1.1/go.mod h1:3QBUiuLct9moP1LgDhCGIg0Ovxn38Bd2sGndnUOuj4o= maunium.net/go/tcell v0.2.0 h1:1Q0kN3wCOGAIGu1r3QHADsjSUOPDylKREvCv3EzJpVg= diff --git a/ui/commands.go b/ui/commands.go index 1877c89..9d38396 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -37,7 +37,6 @@ import ( "github.com/russross/blackfriday/v2" "maunium.net/go/mautrix" - "maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" @@ -366,212 +365,6 @@ func cmdFingerprint(cmd *Command) { } } -// region TODO these four functions currently use the crypto internals directly. switch to interfaces before releasing - -func autocompleteDeviceUserID(cmd *CommandAutocomplete) (completions []string, newText string) { - userCompletions := cmd.Room.AutocompleteUser(cmd.Args[0]) - if len(userCompletions) == 1 { - newText = fmt.Sprintf("/%s %s ", cmd.OrigCommand, userCompletions[0].id) - } else { - completions = make([]string, len(userCompletions)) - for i, completion := range userCompletions { - completions[i] = completion.id - } - } - return -} - -func autocompleteDeviceDeviceID(cmd *CommandAutocomplete) (completions []string, newText string) { - mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) - devices, err := mach.CryptoStore.GetDevices(id.UserID(cmd.Args[0])) - if len(devices) == 0 || err != nil { - return - } - var completedDeviceID id.DeviceID - if len(cmd.Args) > 1 { - existingID := strings.ToUpper(cmd.Args[1]) - for _, device := range devices { - deviceIDStr := string(device.DeviceID) - if deviceIDStr == existingID { - // We don't want to do any autocompletion if there's already a full device ID there. - return []string{}, "" - } else if strings.HasPrefix(strings.ToUpper(device.Name), existingID) || strings.HasPrefix(deviceIDStr, existingID) { - completedDeviceID = device.DeviceID - completions = append(completions, fmt.Sprintf("%s (%s)", device.DeviceID, device.Name)) - } - } - } else { - completions = make([]string, len(devices)) - i := 0 - for _, device := range devices { - completedDeviceID = device.DeviceID - completions[i] = fmt.Sprintf("%s (%s)", device.DeviceID, device.Name) - i++ - } - } - if len(completions) == 1 { - newText = fmt.Sprintf("/%s %s %s ", cmd.OrigCommand, cmd.Args[0], completedDeviceID) - } - return -} - -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{}, "" -} - -func getDevice(cmd *Command) *crypto.DeviceIdentity { - if len(cmd.Args) < 2 { - cmd.Reply("Usage: /%s [fingerprint]", cmd.Command) - return nil - } - mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) - device, err := mach.GetOrFetchDevice(id.UserID(cmd.Args[0]), id.DeviceID(cmd.Args[1])) - if err != nil { - cmd.Reply("Failed to get device: %v", err) - return nil - } - return device -} - -func putDevice(cmd *Command, device *crypto.DeviceIdentity, action string) { - mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) - err := mach.CryptoStore.PutDevice(device.UserID, device) - if err != nil { - cmd.Reply("Failed to save device: %v", err) - } else { - cmd.Reply("Successfully %s %s/%s (%s)", action, device.UserID, device.DeviceID, device.Name) - } - mach.OnDevicesChanged(device.UserID) -} - -func cmdDevices(cmd *Command) { - if len(cmd.Args) == 0 { - cmd.Reply("Usage: /devices ") - return - } - userID := id.UserID(cmd.Args[0]) - mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) - devices, err := mach.CryptoStore.GetDevices(userID) - if err != nil { - cmd.Reply("Failed to get device list: %v", err) - } - if len(devices) == 0 { - cmd.Reply("Fetching device list from server...") - devices = mach.LoadDevices(userID) - } - if len(devices) == 0 { - cmd.Reply("No devices found for %s", userID) - return - } - 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()) - } - resp := buf.String() - cmd.Reply("%s", resp[:len(resp)-1]) -} - -func cmdDevice(cmd *Command) { - device := getDevice(cmd) - if device == nil { - return - } - deviceType := "Device" - if device.Deleted { - deviceType = "Deleted device" - } - 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()) -} - -func cmdVerify(cmd *Command) { - device := getDevice(cmd) - if device == nil { - return - } - if device.Trust == crypto.TrustStateVerified { - cmd.Reply("That device is already verified") - return - } - if len(cmd.Args) == 2 { - mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) - mach.DefaultSASTimeout = 120 * time.Second - modal := NewVerificationModal(cmd.MainView, device, mach.DefaultSASTimeout) - cmd.MainView.ShowModal(modal) - err := mach.NewSimpleSASVerificationWith(device, modal) - if err != nil { - cmd.Reply("Failed to start interactive verification: %v", err) - return - } - } else { - fingerprint := strings.Join(cmd.Args[2:], "") - if string(device.SigningKey) != fingerprint { - cmd.Reply("Mismatching fingerprint") - return - } - action := "verified" - if device.Trust == crypto.TrustStateBlacklisted { - action = "unblacklisted and verified" - } - device.Trust = crypto.TrustStateVerified - putDevice(cmd, device, action) - } -} - -func cmdUnverify(cmd *Command) { - device := getDevice(cmd) - if device == nil { - return - } - if device.Trust == crypto.TrustStateUnset { - cmd.Reply("That device is already not verified") - return - } - action := "unverified" - if device.Trust == crypto.TrustStateBlacklisted { - action = "unblacklisted" - } - device.Trust = crypto.TrustStateUnset - putDevice(cmd, device, action) -} - -func cmdBlacklist(cmd *Command) { - device := getDevice(cmd) - if device == nil { - return - } - if device.Trust == crypto.TrustStateBlacklisted { - cmd.Reply("That device is already blacklisted") - return - } - action := "blacklisted" - if device.Trust == crypto.TrustStateVerified { - action = "unverified and blacklisted" - } - device.Trust = crypto.TrustStateBlacklisted - putDevice(cmd, device, action) -} - -func cmdResetSession(cmd *Command) { - err := cmd.Matrix.Crypto().(*crypto.OlmMachine).CryptoStore.RemoveOutboundGroupSession(cmd.Room.Room.ID) - if err != nil { - cmd.Reply("Failed to remove outbound group session: %v", err) - } else { - cmd.Reply("Removed outbound group session for this room") - } -} - -// endregion - func cmdHeapProfile(cmd *Command) { if len(cmd.Args) == 0 || cmd.Args[0] != "nogc" { runtime.GC() diff --git a/ui/crypto-commands.go b/ui/crypto-commands.go new file mode 100644 index 0000000..232f508 --- /dev/null +++ b/ui/crypto-commands.go @@ -0,0 +1,231 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2020 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// +build cgo + +package ui + +import ( + "fmt" + "strings" + "time" + "unicode" + + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/id" +) + +func autocompleteDeviceUserID(cmd *CommandAutocomplete) (completions []string, newText string) { + userCompletions := cmd.Room.AutocompleteUser(cmd.Args[0]) + if len(userCompletions) == 1 { + newText = fmt.Sprintf("/%s %s ", cmd.OrigCommand, userCompletions[0].id) + } else { + completions = make([]string, len(userCompletions)) + for i, completion := range userCompletions { + completions[i] = completion.id + } + } + return +} + +func autocompleteDeviceDeviceID(cmd *CommandAutocomplete) (completions []string, newText string) { + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + devices, err := mach.CryptoStore.GetDevices(id.UserID(cmd.Args[0])) + if len(devices) == 0 || err != nil { + return + } + var completedDeviceID id.DeviceID + if len(cmd.Args) > 1 { + existingID := strings.ToUpper(cmd.Args[1]) + for _, device := range devices { + deviceIDStr := string(device.DeviceID) + if deviceIDStr == existingID { + // We don't want to do any autocompletion if there's already a full device ID there. + return []string{}, "" + } else if strings.HasPrefix(strings.ToUpper(device.Name), existingID) || strings.HasPrefix(deviceIDStr, existingID) { + completedDeviceID = device.DeviceID + completions = append(completions, fmt.Sprintf("%s (%s)", device.DeviceID, device.Name)) + } + } + } else { + completions = make([]string, len(devices)) + i := 0 + for _, device := range devices { + completedDeviceID = device.DeviceID + completions[i] = fmt.Sprintf("%s (%s)", device.DeviceID, device.Name) + i++ + } + } + if len(completions) == 1 { + newText = fmt.Sprintf("/%s %s %s ", cmd.OrigCommand, cmd.Args[0], completedDeviceID) + } + return +} + +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{}, "" +} + +func getDevice(cmd *Command) *crypto.DeviceIdentity { + if len(cmd.Args) < 2 { + cmd.Reply("Usage: /%s [fingerprint]", cmd.Command) + return nil + } + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + device, err := mach.GetOrFetchDevice(id.UserID(cmd.Args[0]), id.DeviceID(cmd.Args[1])) + if err != nil { + cmd.Reply("Failed to get device: %v", err) + return nil + } + return device +} + +func putDevice(cmd *Command, device *crypto.DeviceIdentity, action string) { + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + err := mach.CryptoStore.PutDevice(device.UserID, device) + if err != nil { + cmd.Reply("Failed to save device: %v", err) + } else { + cmd.Reply("Successfully %s %s/%s (%s)", action, device.UserID, device.DeviceID, device.Name) + } + mach.OnDevicesChanged(device.UserID) +} + +func cmdDevices(cmd *Command) { + if len(cmd.Args) == 0 { + cmd.Reply("Usage: /devices ") + return + } + userID := id.UserID(cmd.Args[0]) + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + devices, err := mach.CryptoStore.GetDevices(userID) + if err != nil { + cmd.Reply("Failed to get device list: %v", err) + } + if len(devices) == 0 { + cmd.Reply("Fetching device list from server...") + devices = mach.LoadDevices(userID) + } + if len(devices) == 0 { + cmd.Reply("No devices found for %s", userID) + return + } + 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()) + } + resp := buf.String() + cmd.Reply("%s", resp[:len(resp)-1]) +} + +func cmdDevice(cmd *Command) { + device := getDevice(cmd) + if device == nil { + return + } + deviceType := "Device" + if device.Deleted { + deviceType = "Deleted device" + } + 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()) +} + +func cmdVerify(cmd *Command) { + device := getDevice(cmd) + if device == nil { + return + } + if device.Trust == crypto.TrustStateVerified { + cmd.Reply("That device is already verified") + return + } + if len(cmd.Args) == 2 { + mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) + mach.DefaultSASTimeout = 120 * time.Second + modal := NewVerificationModal(cmd.MainView, device, mach.DefaultSASTimeout) + cmd.MainView.ShowModal(modal) + _, err := mach.NewSimpleSASVerificationWith(device, modal) + if err != nil { + cmd.Reply("Failed to start interactive verification: %v", err) + return + } + } else { + fingerprint := strings.Join(cmd.Args[2:], "") + if string(device.SigningKey) != fingerprint { + cmd.Reply("Mismatching fingerprint") + return + } + action := "verified" + if device.Trust == crypto.TrustStateBlacklisted { + action = "unblacklisted and verified" + } + device.Trust = crypto.TrustStateVerified + putDevice(cmd, device, action) + } +} + +func cmdUnverify(cmd *Command) { + device := getDevice(cmd) + if device == nil { + return + } + if device.Trust == crypto.TrustStateUnset { + cmd.Reply("That device is already not verified") + return + } + action := "unverified" + if device.Trust == crypto.TrustStateBlacklisted { + action = "unblacklisted" + } + device.Trust = crypto.TrustStateUnset + putDevice(cmd, device, action) +} + +func cmdBlacklist(cmd *Command) { + device := getDevice(cmd) + if device == nil { + return + } + if device.Trust == crypto.TrustStateBlacklisted { + cmd.Reply("That device is already blacklisted") + return + } + action := "blacklisted" + if device.Trust == crypto.TrustStateVerified { + action = "unverified and blacklisted" + } + device.Trust = crypto.TrustStateBlacklisted + putDevice(cmd, device, action) +} + +func cmdResetSession(cmd *Command) { + err := cmd.Matrix.Crypto().(*crypto.OlmMachine).CryptoStore.RemoveOutboundGroupSession(cmd.Room.Room.ID) + if err != nil { + cmd.Reply("Failed to remove outbound group session: %v", err) + } else { + cmd.Reply("Removed outbound group session for this room") + } +} diff --git a/ui/no-crypto-commands.go b/ui/no-crypto-commands.go new file mode 100644 index 0000000..dae85b4 --- /dev/null +++ b/ui/no-crypto-commands.go @@ -0,0 +1,36 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2020 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// +build !cgo + +package ui + +func autocompleteDevice(cmd *CommandAutocomplete) ([]string, string) { + return []string{}, "" +} + +func cmdNoCrypto(cmd *Command) { + cmd.Reply("This gomuks was built without encryption support") +} + +var ( + cmdDevices = cmdNoCrypto + cmdDevice = cmdNoCrypto + cmdVerify = cmdNoCrypto + cmdUnverify = cmdNoCrypto + cmdBlacklist = cmdNoCrypto + cmdResetSession = cmdNoCrypto +) diff --git a/ui/room-view.go b/ui/room-view.go index 38a561c..225e0a9 100644 --- a/ui/room-view.go +++ b/ui/room-view.go @@ -629,8 +629,6 @@ func (view *RoomView) InputTabComplete(text string, cursorOffset int) { return } - debug.Print("Tab completing", cursorOffset, text) - str := runewidth.Truncate(text, cursorOffset, "") word := findWordToTabComplete(str) startIndex := len(str) - len(word) diff --git a/ui/verification-modal.go b/ui/verification-modal.go index b3fbd35..bc529c9 100644 --- a/ui/verification-modal.go +++ b/ui/verification-modal.go @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +// +build cgo + package ui import ( @@ -32,14 +34,17 @@ import ( type EmojiView struct { mauview.SimpleEventHandler - Numbers *[3]uint - Emojis *[7]crypto.VerificationEmoji + Data crypto.SASData } func (e *EmojiView) Draw(screen mauview.Screen) { - if e.Emojis != nil { + if e.Data == nil { + return + } + switch e.Data.Type() { + case event.SASEmoji: width := 10 - for i, emoji := range e.Emojis { + for i, emoji := range e.Data.(crypto.EmojiSASData) { x := i*width + i y := 0 if i >= 4 { @@ -49,9 +54,9 @@ func (e *EmojiView) Draw(screen mauview.Screen) { mauview.Print(screen, string(emoji.Emoji), x, y, width, mauview.AlignCenter, tcell.ColorDefault) mauview.Print(screen, emoji.Description, x, y+1, width, mauview.AlignCenter, tcell.ColorDefault) } - } else if e.Numbers != nil { + case event.SASDecimal: maxWidth := 43 - for i, number := range e.Numbers { + for i, number := range e.Data.(crypto.DecimalSASData) { mauview.Print(screen, strconv.FormatUint(uint64(number), 10), 0, i, maxWidth, mauview.AlignCenter, tcell.ColorDefault) } } @@ -69,6 +74,8 @@ type VerificationModal struct { emojiText *EmojiView inputBar *mauview.InputField + progress int + progressMax int stopWaiting chan struct{} confirmChan chan bool done bool @@ -85,14 +92,15 @@ func NewVerificationModal(mainView *MainView, device *crypto.DeviceIdentity, tim done: false, } - progress := int(timeout.Seconds()) + vm.progressMax = int(timeout.Seconds()) + vm.progress = vm.progressMax vm.waitingBar = mauview.NewProgressBar(). - SetMax(progress). - SetProgress(progress). + SetMax(vm.progressMax). + SetProgress(vm.progress). SetIndeterminate(false) vm.infoText = mauview.NewTextView() - vm.infoText.SetText(fmt.Sprintf("Waiting for %s to accept", device.UserID)) + vm.infoText.SetText(fmt.Sprintf("Waiting for %s\nto accept", device.UserID)) vm.emojiText = &EmojiView{} @@ -113,59 +121,58 @@ func NewVerificationModal(mainView *MainView, device *crypto.DeviceIdentity, tim vm.Component = mauview.Center(vm.container, 45, 12).SetAlwaysFocusChild(true) - go vm.decrementWaitingBar(progress) + go vm.decrementWaitingBar() return vm } -func (vm *VerificationModal) decrementWaitingBar(progress int) { +func (vm *VerificationModal) decrementWaitingBar() { for { select { case <-time.Tick(time.Second): - if progress <= 0 { + if vm.progress <= 0 { + vm.waitingBar.SetIndeterminate(true) + vm.parent.parent.app.SetRedrawTicker(100 * time.Millisecond) return } - progress-- - vm.waitingBar.SetProgress(progress) + vm.progress-- + vm.waitingBar.SetProgress(vm.progress) vm.parent.parent.Render() case <-vm.stopWaiting: - vm.waitingBar.SetIndeterminate(true) - vm.parent.parent.app.SetRedrawTicker(100 * time.Millisecond) return } } } -func (vm *VerificationModal) VerifyEmojisMatch(emojis [7]crypto.VerificationEmoji) bool { - vm.infoText.SetText("Check if the other device is showing the\nsame emojis as below, then type \"yes\" to\naccept, or \"no\" to reject") - vm.inputBar. - SetTextColor(tcell.ColorWhite). - SetBackgroundColor(tcell.ColorDarkCyan). - SetPlaceholder("Type \"yes\" or \"no\""). - Focus() - vm.emojiText.Emojis = &emojis - vm.parent.parent.Render() - vm.stopWaiting <- struct{}{} - confirm := <-vm.confirmChan - vm.emojiText.Emojis = nil - vm.infoText.SetText(fmt.Sprintf("Waiting for %s to accept", vm.device.UserID)) - vm.parent.parent.Render() - return confirm +func (vm *VerificationModal) VerificationMethods() []crypto.VerificationMethod { + return []crypto.VerificationMethod{crypto.VerificationMethodEmoji{}, crypto.VerificationMethodDecimal{}} } -func (vm *VerificationModal) VerifyNumbersMatch(numbers [3]uint) bool { - vm.infoText.SetText("Check if the other device is showing the\nsame numbers as below, then type \"yes\" to\naccept, or \"no\" to reject") +func (vm *VerificationModal) VerifySASMatch(_ *crypto.DeviceIdentity, data crypto.SASData) bool { + var typeName string + if data.Type() == event.SASDecimal { + typeName = "numbers" + } else if data.Type() == event.SASEmoji { + typeName = "emojis" + } else { + return false + } + vm.infoText.SetText(fmt.Sprintf( + "Check if the other device is showing the\n"+ + "same %s as below, then type \"yes\" to\n"+ + "accept, or \"no\" to reject", typeName)) vm.inputBar. SetTextColor(tcell.ColorWhite). SetBackgroundColor(tcell.ColorDarkCyan). SetPlaceholder("Type \"yes\" or \"no\""). Focus() - vm.emojiText.Numbers = &numbers + vm.emojiText.Data = data vm.parent.parent.Render() - vm.stopWaiting <- struct{}{} + vm.progress = vm.progressMax confirm := <-vm.confirmChan - vm.emojiText.Numbers = nil - vm.infoText.SetText(fmt.Sprintf("Waiting for %s to accept", vm.device.UserID)) + vm.progress = vm.progressMax + vm.emojiText.Data = nil + vm.infoText.SetText(fmt.Sprintf("Waiting for %s\nto confirm", vm.device.UserID)) vm.parent.parent.Render() return confirm } @@ -179,6 +186,7 @@ func (vm *VerificationModal) OnCancel(cancelledByUs bool, reason string, _ event vm.infoText.SetText(fmt.Sprintf("Verification cancelled by %s: %s", vm.device.UserID, reason)) } vm.inputBar.SetPlaceholder("Press enter to close the dialog") + vm.stopWaiting <- struct{}{} vm.done = true vm.parent.parent.Render() } @@ -188,6 +196,7 @@ func (vm *VerificationModal) OnSuccess() { vm.parent.parent.app.SetRedrawTicker(1 * time.Minute) vm.infoText.SetText(fmt.Sprintf("Successfully verified %s (%s) of %s", vm.device.Name, vm.device.DeviceID, vm.device.UserID)) vm.inputBar.SetPlaceholder("Press enter to close the dialog") + vm.stopWaiting <- struct{}{} vm.done = true vm.parent.parent.Render() if vm.parent.config.SendToVerifiedOnly { @@ -203,7 +212,7 @@ func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { return true } return false - } else if vm.emojiText.Emojis == nil && vm.emojiText.Numbers == nil { + } else if vm.emojiText.Data == nil { debug.Print("Ignoring pre-emoji key event") return false } @@ -216,8 +225,11 @@ func (vm *VerificationModal) OnKeyEvent(event mauview.KeyEvent) bool { debug.Print("Rejecting verification") vm.confirmChan <- false } - vm.inputBar.SetPlaceholder("") - vm.inputBar.SetTextAndMoveCursor("") + vm.inputBar. + SetPlaceholder(""). + SetTextAndMoveCursor(""). + SetBackgroundColor(tcell.ColorDefault). + SetTextColor(tcell.ColorDefault) return true } else { return vm.inputBar.OnKeyEvent(event) -- cgit v1.2.3