// 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" "io/ioutil" "path/filepath" "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") } } 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) }