feat: new interface

This commit is contained in:
Adrien Waksberg 2019-10-20 11:09:25 +02:00
parent c47c6719d2
commit 6c30928065
3 changed files with 141 additions and 350 deletions

View file

@ -7,6 +7,10 @@ Which is based on [Keep A Changelog](http://keepachangelog.com/)
## Unreleased ## Unreleased
### Changed
- New interface
## v1.2.1 - 2019-10-19 ## v1.2.1 - 2019-10-19
### Fixed ### Fixed

View file

@ -1,332 +1,190 @@
// Copyright 2019 Adrien Waksberg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gpm package gpm
import ( import (
"bufio"
"fmt" "fmt"
"github.com/atotto/clipboard" "log"
"github.com/olekukonko/tablewriter"
"golang.org/x/crypto/ssh/terminal" ui "github.com/gizak/termui/v3"
"io/ioutil" "github.com/gizak/termui/v3/widgets"
"os"
"strconv"
"syscall"
"time"
) )
// Cli contain config and wallet to use
type Cli struct { type Cli struct {
Config Config Config Config
Wallet Wallet Wallet Wallet
} }
// printEntries show entries with tables func (c *Cli) ErrorBox(msg string) {
func (c *Cli) printEntries(entries []Entry) { p := widgets.NewParagraph()
var otp string p.Title = "Notification"
var tables map[string]*tablewriter.Table p.SetRect(10, 0, 70, 5)
p.Text = fmt.Sprintf("[ERROR: %s](fg:red) ", msg)
tables = make(map[string]*tablewriter.Table) ui.Render(p)
for i, entry := range entries {
if entry.OTP == "" {
otp = ""
} else {
otp = "X"
}
if _, present := tables[entry.Group]; present == false {
tables[entry.Group] = tablewriter.NewWriter(os.Stdout)
tables[entry.Group].SetHeader([]string{"", "Name", "URI", "User", "OTP", "Comment"})
tables[entry.Group].SetBorder(false)
tables[entry.Group].SetColumnColor(
tablewriter.Colors{tablewriter.Normal, tablewriter.FgYellowColor},
tablewriter.Colors{tablewriter.Normal, tablewriter.FgWhiteColor},
tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor},
tablewriter.Colors{tablewriter.Normal, tablewriter.FgGreenColor},
tablewriter.Colors{tablewriter.Normal, tablewriter.FgWhiteColor},
tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor})
}
tables[entry.Group].Append([]string{strconv.Itoa(i), entry.Name, entry.URI, entry.User, otp, entry.Comment})
}
for group, table := range tables {
fmt.Printf("\n%s\n\n", group)
table.Render()
fmt.Println("")
}
} }
// error print a message and exit) func (c *Cli) InputBox(msg string, hidden bool) string {
func (c *Cli) error(msg string) { var input, secret string
fmt.Printf("ERROR: %s\n", msg)
os.Exit(2)
}
// input from the console p := widgets.NewParagraph()
func (c *Cli) input(text string, defaultValue string, show bool) string { p.SetRect(10, 10, 70, 5)
fmt.Print(text) p.Text = fmt.Sprintf("%s ", msg)
if show == false { ui.Render(p)
data, _ := terminal.ReadPassword(int(syscall.Stdin))
text := string(data)
fmt.Printf("\n")
if text == "" { uiEvents := ui.PollEvents()
return defaultValue for {
} e := <-uiEvents
return text switch e.ID {
} case "q", "<C-c>":
return ""
input := bufio.NewScanner(os.Stdin) case "<Backspace>":
input.Scan() if len(input) >= 1 {
if input.Text() == "" { input = input[:len(input)-1]
return defaultValue
}
return input.Text()
}
// selectEntry with a form
func (c *Cli) selectEntry() Entry {
var index int
entries := c.Wallet.SearchEntry(*PATTERN, *GROUP)
if len(entries) == 0 {
fmt.Println("no entry found")
os.Exit(1)
}
c.printEntries(entries)
if len(entries) == 1 {
return entries[0]
}
c1 := make(chan int, 1)
go func(max int) {
for true {
index, err := strconv.Atoi(c.input("Select the entry: ", "", true))
if err == nil && index >= 0 && index+1 <= max {
c1 <- index
break
} }
fmt.Println("your choice is not an integer or is out of range") case "<Enter>":
return input
case "<Space>":
input = input + " "
default:
input = input + e.ID
} }
}(len(entries))
select { if hidden {
case res := <-c1: secret = ""
index = res for i := 1; i <= int(float64(len(input)) * 1.75); i++ {
case <-time.After(30 * time.Second): secret = secret + "*"
os.Exit(1) }
p.Text = fmt.Sprintf("%s %s", msg, secret)
} else {
p.Text = fmt.Sprintf("%s %s", msg, input)
}
ui.Render(p)
} }
return entries[index]
} }
// loadWallet get and unlock the wallet func (c *Cli) EntryBox(entry Entry) {
func (c *Cli) loadWallet() { p := widgets.NewParagraph()
p.Title = "Entry"
p.SetRect(25, 0, 80, 23)
p.Text = fmt.Sprintf("%s[Name:](fg:yellow) %s\n", p.Text, entry.Name)
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)
}
func (c *Cli) UnlockWallet(wallet string) error {
var walletName string var walletName string
var err error
passphrase := c.input("Enter the passphrase to unlock the wallet: ", "", false) ui.Clear()
if wallet == "" {
if *WALLET == "" {
walletName = c.Config.WalletDefault walletName = c.Config.WalletDefault
} else { } else {
walletName = *WALLET walletName = wallet
} }
c.Wallet = Wallet{ c.Wallet = Wallet{
Name: walletName, Name: walletName,
Path: fmt.Sprintf("%s/%s.gpm", c.Config.WalletDir, walletName), Path: fmt.Sprintf("%s/%s.gpm", c.Config.WalletDir, walletName),
Passphrase: passphrase,
} }
err := c.Wallet.Load() for i := 0; i < 3; i++ {
if err != nil { c.Wallet.Passphrase = c.InputBox("Enter the passphrase to unlock the wallet:\n", true)
c.error(fmt.Sprintf("%s", err))
err = c.Wallet.Load()
if err == nil {
return nil
}
c.ErrorBox(fmt.Sprintf("%s", err))
} }
return err
} }
// List the entry of a wallet func (c *Cli) GroupsBox() string {
func (c *Cli) listEntry() { return ""
c.loadWallet()
entries := c.Wallet.SearchEntry(*PATTERN, *GROUP)
if len(entries) == 0 {
fmt.Println("no entry found")
os.Exit(1)
} else {
c.printEntries(entries)
}
} }
// Delete an entry of a wallet func (c *Cli) ListEntries() {
func (c *Cli) deleteEntry() { var pattern, group string
var entry Entry var entries []Entry
c.loadWallet() refresh := true
entry = c.selectEntry() index := -1
confirm := c.input("are you sure you want to remove this entry [y/N] ?", "N", true)
if confirm == "y" { l := widgets.NewList()
err := c.Wallet.DeleteEntry(entry.ID) l.Title = "Entries"
if err != nil { l.TextStyle = ui.NewStyle(ui.ColorYellow)
c.error(fmt.Sprintf("%s", err)) 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 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()
} }
err = c.Wallet.Save() if index >= 0 {
if err != nil { c.EntryBox(entries[index])
c.error(fmt.Sprintf("%s", err))
} }
fmt.Println("the entry has been deleted") ui.Render(l)
} e := <-uiEvents
} switch e.ID {
case "q", "<C-c>":
// Add a new entry in wallet return
func (c *Cli) addEntry() { case "<Enter>":
c.loadWallet() index = l.SelectedRow
case "<Escape>":
entry := Entry{} pattern = ""
entry.GenerateID() refresh = true
entry.Name = c.input("Enter the name: ", "", true) case "/":
entry.Group = c.input("Enter the group: ", "", true) pattern = c.InputBox("Search", false)
entry.URI = c.input("Enter the URI: ", "", true) refresh = true
entry.User = c.input("Enter the username: ", "", true) case "g":
if *RANDOM { group = c.GroupsBox()
entry.Password = RandomString(c.Config.PasswordLength, refresh = true
c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial) case "j", "<Down>":
} else { if len(entries) > 0 {
entry.Password = c.input("Enter the new password: ", entry.Password, false) l.ScrollDown()
} }
entry.OTP = c.input("Enter the OTP key: ", "", false) case "k", "<Up>":
entry.Comment = c.input("Enter a comment: ", "", true) if len(entries) > 0 {
l.ScrollUp()
err := c.Wallet.AddEntry(entry)
if err != nil {
c.error(fmt.Sprintf("%s", err))
}
err = c.Wallet.Save()
if err != nil {
c.error(fmt.Sprintf("%s", err))
}
fmt.Println("the entry has been added")
}
// Update an entry in wallet
func (c *Cli) updateEntry() {
c.loadWallet()
entry := c.selectEntry()
entry.Name = c.input("Enter the new name: ", entry.Name, true)
entry.Group = c.input("Enter the new group: ", entry.Group, true)
entry.URI = c.input("Enter the new URI: ", entry.URI, true)
entry.User = c.input("Enter the new username: ", entry.User, true)
if *RANDOM {
entry.Password = RandomString(c.Config.PasswordLength,
c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial)
} else {
entry.Password = c.input("Enter the new password: ", entry.Password, false)
}
entry.OTP = c.input("Enter the new OTP key: ", entry.OTP, false)
entry.Comment = c.input("Enter a new comment: ", entry.Comment, true)
err := c.Wallet.UpdateEntry(entry)
if err != nil {
c.error(fmt.Sprintf("%s", err))
}
c.Wallet.Save()
}
// Copy login and password from an entry
func (c *Cli) copyEntry() {
c.loadWallet()
entry := c.selectEntry()
go func() {
for true {
choice := c.input("select one action: ", "", true)
switch choice {
case "l":
clipboard.WriteAll(entry.User)
case "p":
clipboard.WriteAll(entry.Password)
case "o":
code, time, _ := entry.OTPCode()
fmt.Printf("this OTP code is available for %d seconds\n", time)
clipboard.WriteAll(code)
case "q":
clipboard.WriteAll("")
os.Exit(0)
default:
fmt.Println("l -> copy login")
fmt.Println("p -> copy password")
fmt.Println("o -> copy OTP code")
fmt.Println("q -> quit")
} }
} }
}()
select {
case <-time.After(90 * time.Second):
clipboard.WriteAll("")
os.Exit(1)
} }
} }
// Import entries from json file func Run() {
func (c *Cli) ImportWallet() { var c Cli
c.loadWallet() c.Config.Load("")
_, err := os.Stat(*IMPORT) if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
err := c.UnlockWallet("test")
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) return
} }
data, err := ioutil.ReadFile(*IMPORT) c.ListEntries()
if err != nil {
c.error(fmt.Sprintf("%s", err))
}
err = c.Wallet.Import(data)
if err != nil {
c.error(fmt.Sprintf("%s", err))
}
err = c.Wallet.Save()
if err != nil {
c.error(fmt.Sprintf("%s", err))
}
fmt.Println("the import was successful")
}
// Export a wallet in json format
func (c *Cli) ExportWallet() {
c.loadWallet()
data, err := c.Wallet.Export()
if err != nil {
c.error(fmt.Sprintf("%s", err))
}
err = ioutil.WriteFile(*EXPORT, data, 0600)
if err != nil {
c.error(fmt.Sprintf("%s", err))
}
fmt.Println("the export was successful")
} }

View file

@ -1,71 +0,0 @@
// Copyright 2019 Adrien Waksberg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gpm
import (
"flag"
"fmt"
"os"
)
// Options
var (
ADD = flag.Bool("add", false, "add a new entry in the wallet")
UPDATE = flag.Bool("update", false, "update an entry")
DELETE = flag.Bool("delete", false, "delete an entry")
LIST = flag.Bool("list", false, "list the entries in a wallet")
LENGTH = flag.Int("length", 16, "specify the password length")
COPY = flag.Bool("copy", false, "enter an copy mode for an entry")
CONFIG = flag.String("config", "", "specify the config file")
GROUP = flag.String("group", "", "search the entries in this group ")
WALLET = flag.String("wallet", "", "specify the wallet")
PATTERN = flag.String("pattern", "", "search the entries with this pattern")
RANDOM = flag.Bool("random", false, "generate a random password for a new entry or an update")
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")
)
// Run the cli interface
func Run() {
var cli Cli
cli.Config.Load(*CONFIG)
flag.Parse()
if *HELP {
flag.PrintDefaults()
os.Exit(1)
} else if *PASSWD {
fmt.Println(RandomString(*LENGTH, *LETTER, *DIGIT, *SPECIAL))
} else if *LIST {
cli.listEntry()
} else if *COPY {
cli.copyEntry()
} else if *ADD {
cli.addEntry()
} else if *UPDATE {
cli.updateEntry()
} else if *DELETE {
cli.deleteEntry()
} else if *IMPORT != "" {
cli.ImportWallet()
} else if *EXPORT != "" {
cli.ExportWallet()
}
}