commit
2aeaff5b97
2
go.mod
2
go.mod
@ -23,7 +23,7 @@ require (
|
|||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
|
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
|
||||||
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2
|
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2
|
||||||
gopkg.in/yaml.v2 v2.3.0
|
gopkg.in/yaml.v2 v2.3.0
|
||||||
maunium.net/go/mautrix v0.7.1
|
maunium.net/go/mautrix v0.7.4
|
||||||
maunium.net/go/mauview v0.1.1
|
maunium.net/go/mauview v0.1.1
|
||||||
maunium.net/go/tcell v0.2.0
|
maunium.net/go/tcell v0.2.0
|
||||||
)
|
)
|
||||||
|
5
go.sum
5
go.sum
@ -75,6 +75,7 @@ github.com/zyedidia/clipboard v0.0.0-20200421031010-7c45b8673834/go.mod h1:zykFn
|
|||||||
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
|
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
|
||||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
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 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
|
||||||
@ -118,6 +119,10 @@ maunium.net/go/mautrix v0.7.0 h1:9Wxs5S4Wl4S99dbBwfLZYAe/sP7VKaFikw9Ocf88kfk=
|
|||||||
maunium.net/go/mautrix v0.7.0/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
maunium.net/go/mautrix v0.7.0/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
||||||
maunium.net/go/mautrix v0.7.1 h1:ctoAVWUPs0D1AADzuA3KLmaRONnSY61mdUoP2smNqe4=
|
maunium.net/go/mautrix v0.7.1 h1:ctoAVWUPs0D1AADzuA3KLmaRONnSY61mdUoP2smNqe4=
|
||||||
maunium.net/go/mautrix v0.7.1/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
maunium.net/go/mautrix v0.7.1/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
||||||
|
maunium.net/go/mautrix v0.7.3 h1:yC287SXL0pTZzAtpGIvwtlxPHZsWMuq2DmI5/POTKFE=
|
||||||
|
maunium.net/go/mautrix v0.7.3/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
||||||
|
maunium.net/go/mautrix v0.7.4 h1:MDjrvDyHcu5ozKAa80ohcXmYAXZTHgHxrhiERtvkEdY=
|
||||||
|
maunium.net/go/mautrix v0.7.4/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo=
|
||||||
maunium.net/go/mauview v0.1.1 h1:wfTXyPx3LGAGpTskh+UbBv/QItUWnEpaneHmywoYnfY=
|
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/mauview v0.1.1/go.mod h1:3QBUiuLct9moP1LgDhCGIg0Ovxn38Bd2sGndnUOuj4o=
|
||||||
maunium.net/go/tcell v0.2.0 h1:1Q0kN3wCOGAIGu1r3QHADsjSUOPDylKREvCv3EzJpVg=
|
maunium.net/go/tcell v0.2.0 h1:1Q0kN3wCOGAIGu1r3QHADsjSUOPDylKREvCv3EzJpVg=
|
||||||
|
@ -105,10 +105,13 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
|||||||
"o": {"open"},
|
"o": {"open"},
|
||||||
},
|
},
|
||||||
autocompleters: map[string]CommandAutocompleter{
|
autocompleters: map[string]CommandAutocompleter{
|
||||||
"devices": autocompleteDevice,
|
"devices": autocompleteDevice,
|
||||||
"device": autocompleteDevice,
|
"device": autocompleteDevice,
|
||||||
"verify": autocompleteDevice,
|
"verify": autocompleteDevice,
|
||||||
"unverify": autocompleteDevice,
|
"unverify": autocompleteDevice,
|
||||||
|
"import": autocompleteFile,
|
||||||
|
"export": autocompleteFile,
|
||||||
|
"export-room": autocompleteFile,
|
||||||
},
|
},
|
||||||
commands: map[string]CommandHandler{
|
commands: map[string]CommandHandler{
|
||||||
"unknown-command": cmdUnknownCommand,
|
"unknown-command": cmdUnknownCommand,
|
||||||
@ -159,6 +162,9 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
|
|||||||
"unverify": cmdUnverify,
|
"unverify": cmdUnverify,
|
||||||
"blacklist": cmdBlacklist,
|
"blacklist": cmdBlacklist,
|
||||||
"reset-session": cmdResetSession,
|
"reset-session": cmdResetSession,
|
||||||
|
"import": cmdImportKeys,
|
||||||
|
"export": cmdExportKeys,
|
||||||
|
"export-room": cmdExportRoomKeys,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -461,6 +461,11 @@ Things: rooms, users, baremessages, images, typingnotif, unverified
|
|||||||
/verify <user id> <device id> [fingerprint]
|
/verify <user id> <device id> [fingerprint]
|
||||||
- Verify a device. If the fingerprint is not provided,
|
- Verify a device. If the fingerprint is not provided,
|
||||||
interactive emoji verification will be started.
|
interactive emoji verification will be started.
|
||||||
|
/reset-session - Reset the outbound Megolm session in the current room.
|
||||||
|
|
||||||
|
/import <file> - Import encryption keys
|
||||||
|
/export <file> - Export encryption keys
|
||||||
|
/export-room <file> - Export encryption keys for the current room.
|
||||||
|
|
||||||
# Rooms
|
# Rooms
|
||||||
/pm <user id> <...> - Create a private chat with the given user(s).
|
/pm <user id> <...> - Create a private chat with the given user(s).
|
||||||
|
@ -20,6 +20,8 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
@ -229,3 +231,105 @@ func cmdResetSession(cmd *Command) {
|
|||||||
cmd.Reply("Removed outbound group session for this room")
|
cmd.Reply("Removed outbound group session for this room")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func autocompleteFile(cmd *CommandAutocomplete) (completions []string, newText string) {
|
||||||
|
inputPath, err := filepath.Abs(cmd.RawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchNamePrefix, searchDir string
|
||||||
|
if strings.HasSuffix(cmd.RawArgs, "/") {
|
||||||
|
searchDir = inputPath
|
||||||
|
} else {
|
||||||
|
searchNamePrefix = filepath.Base(inputPath)
|
||||||
|
searchDir = filepath.Dir(inputPath)
|
||||||
|
}
|
||||||
|
files, err := ioutil.ReadDir(searchDir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
if !strings.HasPrefix(name, searchNamePrefix) || (name[0] == '.' && searchNamePrefix == "") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fullPath := filepath.Join(searchDir, name)
|
||||||
|
if file.IsDir() {
|
||||||
|
fullPath += "/"
|
||||||
|
}
|
||||||
|
completions = append(completions, fullPath)
|
||||||
|
}
|
||||||
|
if len(completions) == 1 {
|
||||||
|
newText = fmt.Sprintf("/%s %s", cmd.OrigCommand, completions[0])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdImportKeys(cmd *Command) {
|
||||||
|
path, err := filepath.Abs(cmd.RawArgs)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to get absolute path: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to read %s: %v", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
passphrase, ok := cmd.MainView.AskPassword("Key import", false)
|
||||||
|
if !ok {
|
||||||
|
cmd.Reply("Passphrase entry cancelled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||||
|
imported, total, err := mach.ImportKeys(passphrase, data)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to import sessions: %v", err)
|
||||||
|
} else {
|
||||||
|
cmd.Reply("Successfully imported %d/%d sessions", imported, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportKeys(cmd *Command, sessions []*crypto.InboundGroupSession) {
|
||||||
|
path, err := filepath.Abs(cmd.RawArgs)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to get absolute path: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
passphrase, ok := cmd.MainView.AskPassword("Key export", true)
|
||||||
|
if !ok {
|
||||||
|
cmd.Reply("Passphrase entry cancelled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
export, err := crypto.ExportKeys(passphrase, sessions)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to export sessions: %v", err)
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(path, export, 0400)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to write sessions to %s: %v", path, err)
|
||||||
|
} else {
|
||||||
|
cmd.Reply("Successfully exported %d sessions to %s", len(sessions), path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdExportKeys(cmd *Command) {
|
||||||
|
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||||
|
sessions, err := mach.CryptoStore.GetAllGroupSessions()
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to get sessions to export: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exportKeys(cmd, sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdExportRoomKeys(cmd *Command) {
|
||||||
|
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
|
||||||
|
sessions, err := mach.CryptoStore.GetGroupSessionsForRoom(cmd.Room.MxRoom().ID)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Reply("Failed to get sessions to export: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exportKeys(cmd, sessions)
|
||||||
|
}
|
||||||
|
@ -22,6 +22,10 @@ func autocompleteDevice(cmd *CommandAutocomplete) ([]string, string) {
|
|||||||
return []string{}, ""
|
return []string{}, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func autocompleteFile(cmd *CommandAutocomplete) ([]string, string) {
|
||||||
|
return []string{}, ""
|
||||||
|
}
|
||||||
|
|
||||||
func cmdNoCrypto(cmd *Command) {
|
func cmdNoCrypto(cmd *Command) {
|
||||||
cmd.Reply("This gomuks was built without encryption support")
|
cmd.Reply("This gomuks was built without encryption support")
|
||||||
}
|
}
|
||||||
@ -33,4 +37,7 @@ var (
|
|||||||
cmdUnverify = cmdNoCrypto
|
cmdUnverify = cmdNoCrypto
|
||||||
cmdBlacklist = cmdNoCrypto
|
cmdBlacklist = cmdNoCrypto
|
||||||
cmdResetSession = cmdNoCrypto
|
cmdResetSession = cmdNoCrypto
|
||||||
|
cmdImportKeys = cmdNoCrypto
|
||||||
|
cmdExportKeys = cmdNoCrypto
|
||||||
|
cmdExportRoomKeys = cmdNoCrypto
|
||||||
)
|
)
|
||||||
|
133
ui/password-modal.go
Normal file
133
ui/password-modal.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maunium.net/go/mauview"
|
||||||
|
"maunium.net/go/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PasswordModal struct {
|
||||||
|
mauview.Component
|
||||||
|
|
||||||
|
outputChan chan string
|
||||||
|
cancelChan chan struct{}
|
||||||
|
|
||||||
|
form *mauview.Form
|
||||||
|
|
||||||
|
text *mauview.TextField
|
||||||
|
confirmText *mauview.TextField
|
||||||
|
|
||||||
|
input *mauview.InputField
|
||||||
|
confirmInput *mauview.InputField
|
||||||
|
|
||||||
|
cancel *mauview.Button
|
||||||
|
submit *mauview.Button
|
||||||
|
|
||||||
|
parent *MainView
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *MainView) AskPassword(title string, isNew bool) (string, bool) {
|
||||||
|
pwm := NewPasswordModal(view, title, isNew)
|
||||||
|
view.ShowModal(pwm)
|
||||||
|
return pwm.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPasswordModal(parent *MainView, title string, isNew bool) *PasswordModal {
|
||||||
|
pwm := &PasswordModal{
|
||||||
|
parent: parent,
|
||||||
|
form: mauview.NewForm(),
|
||||||
|
outputChan: make(chan string, 1),
|
||||||
|
cancelChan: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
pwm.form.
|
||||||
|
SetColumns([]int{1, 20, 1, 20, 1}).
|
||||||
|
SetRows([]int{1, 1, 1, 0, 0, 0, 1, 1, 1})
|
||||||
|
|
||||||
|
width := 45
|
||||||
|
height := 8
|
||||||
|
|
||||||
|
pwm.text = mauview.NewTextField()
|
||||||
|
if isNew {
|
||||||
|
pwm.text.SetText("Create a passphrase")
|
||||||
|
} else {
|
||||||
|
pwm.text.SetText("Enter the passphrase")
|
||||||
|
}
|
||||||
|
pwm.input = mauview.NewInputField().
|
||||||
|
SetMaskCharacter('*').
|
||||||
|
SetPlaceholder("correct horse battery staple")
|
||||||
|
pwm.form.AddComponent(pwm.text, 1, 1, 3, 1)
|
||||||
|
pwm.form.AddFormItem(pwm.input, 1, 2, 3, 1)
|
||||||
|
|
||||||
|
if isNew {
|
||||||
|
height += 3
|
||||||
|
pwm.confirmInput = mauview.NewInputField().
|
||||||
|
SetMaskCharacter('*').
|
||||||
|
SetPlaceholder("correct horse battery staple").
|
||||||
|
SetChangedFunc(pwm.HandleChange)
|
||||||
|
pwm.input.SetChangedFunc(pwm.HandleChange)
|
||||||
|
pwm.confirmText = mauview.NewTextField().SetText("Confirm passphrase")
|
||||||
|
|
||||||
|
pwm.form.SetRow(3, 1).SetRow(4, 1).SetRow(5, 1)
|
||||||
|
pwm.form.AddComponent(pwm.confirmText, 1, 4, 3, 1)
|
||||||
|
pwm.form.AddFormItem(pwm.confirmInput, 1, 5, 3, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pwm.cancel = mauview.NewButton("Cancel").SetOnClick(pwm.ClickCancel)
|
||||||
|
pwm.submit = mauview.NewButton("Submit").SetOnClick(pwm.ClickSubmit)
|
||||||
|
|
||||||
|
pwm.form.AddFormItem(pwm.submit, 3, 7, 1, 1)
|
||||||
|
pwm.form.AddFormItem(pwm.cancel, 1, 7, 1, 1)
|
||||||
|
|
||||||
|
box := mauview.NewBox(pwm.form).SetTitle(title)
|
||||||
|
center := mauview.Center(box, width, height).SetAlwaysFocusChild(true)
|
||||||
|
center.Focus()
|
||||||
|
pwm.form.FocusNextItem()
|
||||||
|
pwm.Component = center
|
||||||
|
|
||||||
|
return pwm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pwm *PasswordModal) HandleChange(_ string) {
|
||||||
|
if pwm.input.GetText() == pwm.confirmInput.GetText() {
|
||||||
|
pwm.submit.SetBackgroundColor(mauview.Styles.ContrastBackgroundColor)
|
||||||
|
} else {
|
||||||
|
pwm.submit.SetBackgroundColor(tcell.ColorDefault)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pwm *PasswordModal) ClickCancel() {
|
||||||
|
pwm.parent.HideModal()
|
||||||
|
pwm.cancelChan <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pwm *PasswordModal) ClickSubmit() {
|
||||||
|
if pwm.confirmInput == nil || pwm.input.GetText() == pwm.confirmInput.GetText() {
|
||||||
|
pwm.parent.HideModal()
|
||||||
|
pwm.outputChan <- pwm.input.GetText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pwm *PasswordModal) Wait() (string, bool) {
|
||||||
|
select {
|
||||||
|
case result := <- pwm.outputChan:
|
||||||
|
return result, true
|
||||||
|
case <- pwm.cancelChan:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user