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 (limited to 'ui') 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