gpm/gpm/cli.go

507 lines
11 KiB
Go

package gpm
import (
"fmt"
"flag"
"io/ioutil"
"os"
"time"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
"github.com/atotto/clipboard"
)
// Options
var (
LENGTH = flag.Int("length", 16, "specify the password length")
CONFIG = flag.String("config", "", "specify the config file")
WALLET = flag.String("wallet", "", "specify the wallet")
PASSWD = flag.Bool("password", false, "generate and print a random password")
DIGIT = flag.Bool("digit", false, "use digit to generate a random password")
LETTER = flag.Bool("letter", false, "use letter to generate a random password")
SPECIAL = flag.Bool("special", false, "use special chars to generate a random password")
EXPORT = flag.String("export", "", "json file path to export a wallet")
IMPORT = flag.String("import", "", "json file path to import entries")
HELP = flag.Bool("help", false, "print this help message")
)
// Cli struct
type Cli struct {
Config Config
Wallet Wallet
}
// NotificationBox print a notification
func (c *Cli) NotificationBox(msg string, error bool) {
p := widgets.NewParagraph()
p.SetRect(25, 20, 80, 23)
if error {
p.Title = "Error"
p.Text = fmt.Sprintf("[%s](fg:red) ", msg)
} else {
p.Title = "Notification"
p.Text = fmt.Sprintf("[%s](fg:green) ", msg)
}
ui.Render(p)
}
// ChoiceBox is a boolean form
func (c *Cli) ChoiceBox(title string, choice bool) bool {
t := widgets.NewTabPane("Yes", "No")
t.SetRect(10, 10, 70, 5)
t.Title = title
t.Border = true
if !choice {
t.ActiveTabIndex = 1
}
uiEvents := ui.PollEvents()
for {
ui.Render(t)
e := <-uiEvents
switch e.ID {
case "<Enter>":
return choice
case "<Left>", "h":
t.FocusLeft()
choice = true
case "<Right>", "l":
t.FocusRight()
choice = false
}
}
}
// InputBox is string form
func (c *Cli) InputBox(title string, input string, hidden bool) string {
var secret string
p := widgets.NewParagraph()
p.SetRect(10, 10, 70, 5)
p.Title = title
p.Text = input
uiEvents := ui.PollEvents()
for {
ui.Render(p)
e := <-uiEvents
switch e.ID {
case "q", "<C-c>":
return ""
case "<Backspace>":
if len(input) >= 1 {
input = input[:len(input)-1]
}
case "<Enter>":
return input
case "<Space>":
input = input + " "
default:
input = input + e.ID
}
if hidden {
secret = ""
for i := 1; i <= int(float64(len(input)) * 1.75); i++ {
secret = secret + "*"
}
p.Text = secret
} else {
p.Text = input
}
}
}
// EntryBox to add a new entry
func (c *Cli) EntryBox(entry Entry) {
p := widgets.NewParagraph()
p.SetRect(25, 0, 80, 20)
p.Text = fmt.Sprintf("%s[Name:](fg:yellow) %s\n", p.Text, entry.Name)
p.Text = fmt.Sprintf("%s[Group:](fg:yellow) %s\n", p.Text, entry.Group)
p.Text = fmt.Sprintf("%s[URI:](fg:yellow) %s\n", p.Text, entry.URI)
p.Text = fmt.Sprintf("%s[User:](fg:yellow) %s\n", p.Text, entry.User)
if entry.OTP == "" {
p.Text = fmt.Sprintf("%s[OTP:](fg:yellow) [no](fg:red)\n", p.Text)
} else {
p.Text = fmt.Sprintf("%s[OTP:](fg:yellow) [yes](fg:green)\n", p.Text)
}
p.Text = fmt.Sprintf("%s[Comment:](fg:yellow) %v\n", p.Text, entry.Comment)
ui.Render(p)
}
// GroupBox to select a group
func (c *Cli) GroupsBox() string {
l := widgets.NewList()
l.Title = "Groups"
l.TextStyle = ui.NewStyle(ui.ColorYellow)
l.SelectedRowStyle = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierBold)
l.WrapText = false
l.SetRect(0, 0, 25, 23)
l.Rows = c.Wallet.Groups()
uiEvents := ui.PollEvents()
for {
ui.Render(l)
e := <-uiEvents
switch e.ID {
case "q", "<C-c>", "<Escape>":
return ""
case "<Enter>":
if len(l.Rows) == 0 {
return ""
} else {
return l.Rows[l.SelectedRow]
}
case "j", "<Down>":
if len(l.Rows) > 0 {
l.ScrollDown()
}
case "k", "<Up>":
if len(l.Rows) > 0 {
l.ScrollUp()
}
}
}
}
// HelpBox print help message
func (c *Cli) HelpBox() {
p := widgets.NewParagraph()
p.SetRect(25, 0, 80, 20)
p.Title = "Short keys"
p.Text = `[<escape>](fg:yellow) clear current search
[<enter> ](fg:yellow) select entry or group
[<up> ](fg:yellow) move cursor to up
[<down> ](fg:yellow) move cursor to down
[h ](fg:yellow) print this help message
[q ](fg:yellow) quit
[g ](fg:yellow) filter the entries by group
[n ](fg:yellow) add a new entry
[u ](fg:yellow) update an entry
[d ](fg:yellow) delete an entry
[/ ](fg:yellow) search
[Ctrl + b](fg:yellow) copy username
[Ctrl + c](fg:yellow) copy password
[Ctrl + o](fg:yellow) copy OTP code
`
ui.Render(p)
}
// UnlockWallet to decrypt a wallet
func (c *Cli) UnlockWallet(wallet string) error {
var walletName string
var err error
ui.Clear()
if wallet == "" {
walletName = c.Config.WalletDefault
} else {
walletName = wallet
}
c.Wallet = Wallet{
Name: walletName,
Path: fmt.Sprintf("%s/%s.gpm", c.Config.WalletDir, walletName),
}
for i := 0; i < 3; i++ {
c.Wallet.Passphrase = c.InputBox("Passphrase to unlock the wallet", "", true)
err = c.Wallet.Load()
if err == nil {
return nil
}
c.NotificationBox(fmt.Sprintf("%s", err), true)
}
return err
}
// DeleteEntry to delete an exisiting entry
func (c *Cli) DeleteEntry(entry Entry) bool {
if !c.ChoiceBox("Do you want delete this entry ?", false) {
return false
}
err := c.Wallet.DeleteEntry(entry.ID)
if err != nil {
c.NotificationBox(fmt.Sprintf("%s", err), true)
return false
}
err = c.Wallet.Save()
if err != nil {
c.NotificationBox(fmt.Sprintf("%s", err), true)
return false
}
return true
}
// UpdateEntry to update an existing entry
func (c *Cli) UpdateEntry(entry Entry) bool {
entry.Name = c.InputBox("Name", entry.Name, false)
entry.Group = c.InputBox("Group", entry.Group, false)
entry.URI = c.InputBox("URI", entry.URI, false)
entry.User = c.InputBox("Username", entry.User, false)
if c.ChoiceBox("Generate a new random password ?", false) {
entry.Password = RandomString(c.Config.PasswordLength,
c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial)
}
entry.Password = c.InputBox("Password", "", true)
entry.OTP = c.InputBox("OTP Key", entry.OTP, false)
entry.Comment = c.InputBox("Comment", entry.Comment, false)
err := c.Wallet.UpdateEntry(entry)
if err != nil {
c.NotificationBox(fmt.Sprintf("%s", err), true)
return false
}
err = c.Wallet.Save()
if err != nil {
c.NotificationBox(fmt.Sprintf("%s", err), true)
return false
}
return true
}
// AddEntry to add new entry
func (c *Cli) AddEntry() bool {
entry := Entry{}
entry.GenerateID()
entry.Name = c.InputBox("Name", "", false)
entry.Group = c.InputBox("Group", "", false)
entry.URI = c.InputBox("URI", "", false)
entry.User = c.InputBox("Username", "", false)
if c.ChoiceBox("Generate a random password ?", true) {
entry.Password = RandomString(c.Config.PasswordLength,
c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial)
} else {
entry.Password = c.InputBox("Password", "", true)
}
entry.OTP = c.InputBox("OTP Key", "", false)
entry.Comment = c.InputBox("Comment", "", false)
err := c.Wallet.AddEntry(entry)
if err != nil {
c.NotificationBox(fmt.Sprintf("%s", err), true)
return false
}
err = c.Wallet.Save()
if err != nil {
c.NotificationBox(fmt.Sprintf("%s", err), true)
return false
}
return true
}
// ListEntries to list all entries
func (c *Cli) ListEntries(ch chan<- bool) {
var pattern, group string
var entries []Entry
var selected bool
refresh := true
index := -1
l := widgets.NewList()
l.TextStyle = ui.NewStyle(ui.ColorYellow)
l.SelectedRowStyle = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierBold)
l.WrapText = false
l.SetRect(0, 0, 25, 23)
ui.Clear()
uiEvents := ui.PollEvents()
for {
if group != "" {
l.Title = fmt.Sprintf("Group: %s", group)
} else {
l.Title = "Group: All"
}
if refresh {
refresh = false
index = -1
entries = c.Wallet.SearchEntry(pattern, group)
l.Rows = []string{}
for _, entry := range entries {
l.Rows = append(l.Rows, entry.Name)
}
ui.Clear()
c.NotificationBox("pess h to view short keys", false)
}
if len(entries) > 0 && index >= 0 && index < len(entries) {
selected = true
} else {
selected = false
}
if selected {
c.EntryBox(entries[index])
}
ui.Render(l)
e := <-uiEvents
switch e.ID {
case "h":
c.HelpBox()
case "q":
clipboard.WriteAll("")
ch <- true
case "<Enter>":
index = l.SelectedRow
case "<Escape>":
pattern = ""
group = ""
refresh = true
case "n":
refresh = c.AddEntry()
case "u":
if selected {
refresh = c.UpdateEntry(entries[index])
}
case "d":
if selected {
refresh = c.DeleteEntry(entries[index])
}
case "/":
pattern = c.InputBox("Search", pattern, false)
refresh = true
case "g":
group = c.GroupsBox()
refresh = true
case "j", "<Down>":
if len(entries) > 0 {
l.ScrollDown()
}
case "k", "<Up>":
if len(entries) > 0 {
l.ScrollUp()
}
case "<C-b>":
if selected {
clipboard.WriteAll(entries[index].User)
}
case "<C-c>":
if selected {
clipboard.WriteAll(entries[index].Password)
}
case "<C-o>":
if selected {
code, time, _ := entries[index].OTPCode()
c.NotificationBox(fmt.Sprintf("the OTP code is available for %d seconds", time), false)
clipboard.WriteAll(code)
}
}
ch <- false
}
}
// Import entries from json file
func (c *Cli) ImportWallet() error {
_, err := os.Stat(*IMPORT)
if err != nil {
return err
}
data, err := ioutil.ReadFile(*IMPORT)
if err != nil {
return err
}
err = c.Wallet.Import(data)
if err != nil {
return err
}
err = c.Wallet.Save()
if err != nil {
return err
}
return nil
}
// Export a wallet in json format
func (c *Cli) ExportWallet() error {
data, err := c.Wallet.Export()
if err != nil {
return err
}
err = ioutil.WriteFile(*EXPORT, data, 0600)
if err != nil {
return err
}
return nil
}
// Run the cli interface
func Run() {
var c Cli
flag.Parse()
c.Config.Load(*CONFIG)
if *HELP {
flag.PrintDefaults()
os.Exit(1)
} else if *PASSWD {
fmt.Println(RandomString(*LENGTH, *LETTER, *DIGIT, *SPECIAL))
os.Exit(0)
}
if err := ui.Init(); err != nil {
fmt.Printf("failed to initialize termui: %v\n", err)
os.Exit(2)
}
defer ui.Close()
err := c.UnlockWallet(*WALLET)
if err != nil {
ui.Close()
fmt.Printf("failed to open the wallet: %v\n", err)
os.Exit(2)
}
if *IMPORT != "" {
err := c.ImportWallet()
if err != nil {
ui.Close()
fmt.Printf("failed to import: %v\n", err)
os.Exit(2)
}
} else if *EXPORT != "" {
err := c.ExportWallet()
if err != nil {
ui.Close()
fmt.Printf("failed to export: %v\n", err)
os.Exit(2)
}
} else {
c1 := make(chan bool)
go c.ListEntries(c1)
for {
select {
case res := <-c1:
if res {
return
}
case <-time.After(300 * time.Second):
return
}
}
}
}