Compare commits
No commits in common. "master" and "v1.2.0" have entirely different histories.
19 changed files with 485 additions and 688 deletions
5
.travis.yml
Normal file
5
.travis.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
language: go
|
||||||
|
script:
|
||||||
|
- go test ./...
|
||||||
|
- go get git.yaegashi.fr/nishiki/gpm/cmd/gpm
|
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -7,27 +7,6 @@ Which is based on [Keep A Changelog](http://keepachangelog.com/)
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Update libs
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Choice box for generate new password was ignored
|
|
||||||
- Bug in search bar
|
|
||||||
|
|
||||||
## v2.0.0 - 2020-12-23
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- New interface
|
|
||||||
|
|
||||||
## v1.2.1 - 2019-10-19
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Bug in select item
|
|
||||||
|
|
||||||
## v1.2.0 - 2019-09-01
|
## v1.2.0 - 2019-09-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -56,7 +35,7 @@ Which is based on [Keep A Changelog](http://keepachangelog.com/)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Prefix error message with ERROR
|
- Prefix error message with ERROR
|
||||||
- Fix new line with clear input
|
- Fix new line with clear input
|
||||||
- Replace sha1 to sha512 in pbkdf2.Key function
|
- Replace sha1 to sha512 in pbkdf2.Key function
|
||||||
- Replace default config directory
|
- Replace default config directory
|
||||||
|
|
81
README.md
81
README.md
|
@ -1,9 +1,9 @@
|
||||||
# gpm: Go Passwords Manager
|
# gpm: Go Passwords Manager
|
||||||
|
|
||||||
[![Version](https://img.shields.io/badge/latest_version-2.0.0-green.svg)](https://code.waks.be/nishiki/gpm/releases)
|
[![Version](https://img.shields.io/badge/latest_version-1.2.0-green.svg)](https://git.yaegashi.fr/nishiki/gpm/releases)
|
||||||
[![Build Status](https://travis-ci.org/nishiki/gpm.svg?branch=master)](https://travis-ci.org/nishiki/gpm)
|
[![Build Status](https://travis-ci.org/nishiki/gpm.svg?branch=master)](https://travis-ci.org/nishiki/gpm)
|
||||||
[![GoReport](https://goreportcard.com/badge/code.waks.be/nishiki/gpm)](https://goreportcard.com/report/code.waks.be/nishiki/gpm)
|
[![GoReport](https://goreportcard.com/badge/git.yaegashi.fr/nishiki/gpm)](https://goreportcard.com/report/git.yaegashi.fr/nishiki/gpm)
|
||||||
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://code.waks.be/nishiki/gpm/src/branch/master/LICENSE)
|
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://git.yaegashi.fr/nishiki/gpm/src/branch/master/LICENSE)
|
||||||
|
|
||||||
gpm is passwords manager write in go and use AES-256 to encrypt the wallets
|
gpm is passwords manager write in go and use AES-256 to encrypt the wallets
|
||||||
|
|
||||||
|
@ -21,40 +21,83 @@ gpm is passwords manager write in go and use AES-256 to encrypt the wallets
|
||||||
- Download and build
|
- Download and build
|
||||||
|
|
||||||
```text
|
```text
|
||||||
go get code.waks.be/nishiki/gpm/cmd/gpm
|
go get git.yaegashi.fr/nishiki/gpm/cmd/gpm
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
### First launch
|
### First steps
|
||||||
|
|
||||||
- Run `gpm`
|
- Add new entry `gpm -add`
|
||||||
- Enter the passphrase to encrypt your new wallet
|
|
||||||
- Press `n` to create your first entry and follow the instructions
|
```text
|
||||||
|
Enter the passphrase to unlock the wallet:
|
||||||
|
Enter the name: Test
|
||||||
|
Enter the group: MyGroup
|
||||||
|
Enter the URI: http://localhost
|
||||||
|
Enter the username: lastname
|
||||||
|
Enter the new password:
|
||||||
|
Enter the OTP key:
|
||||||
|
Enter a comment: My first entry
|
||||||
|
the entry has been added
|
||||||
|
```
|
||||||
|
|
||||||
|
- Search and copy `gpm -copy`
|
||||||
|
|
||||||
|
```text
|
||||||
|
Enter the passphrase to unlock the wallet:
|
||||||
|
|
||||||
|
MyGroup
|
||||||
|
|
||||||
|
| NAME | URI | USER | OTP | COMMENT
|
||||||
|
----+------+------------------+----------+-----+-----------------
|
||||||
|
0 | Test | http://localhost | lastname | X | My first entry
|
||||||
|
|
||||||
|
select one action: p
|
||||||
|
select one action: l
|
||||||
|
select one action: q
|
||||||
|
```
|
||||||
|
|
||||||
### All options
|
### All options
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
gpm -help
|
||||||
|
-add
|
||||||
|
add a new entry in the wallet
|
||||||
-config string
|
-config string
|
||||||
specify the config file
|
specify the config file
|
||||||
|
-copy
|
||||||
|
enter an copy mode for an entry
|
||||||
|
-delete
|
||||||
|
delete an entry
|
||||||
-digit
|
-digit
|
||||||
use digit to generate a random password
|
use digit to generate a random password
|
||||||
-export string
|
-export
|
||||||
json file path to export a wallet
|
export a wallet in json format
|
||||||
|
-group string
|
||||||
|
search the entries in this group
|
||||||
-help
|
-help
|
||||||
print this help message
|
print this help message
|
||||||
-import string
|
-import string
|
||||||
json file path to import entries
|
import entries from a json file
|
||||||
-length int
|
-length int
|
||||||
specify the password length (default 16)
|
specify the password length (default 16)
|
||||||
-letter
|
-letter
|
||||||
use letter to generate a random password
|
use letter to generate a random password
|
||||||
|
-list
|
||||||
|
list the entries in a wallet
|
||||||
-password
|
-password
|
||||||
generate and print a random password
|
generate and print a random password
|
||||||
|
-pattern string
|
||||||
|
search the entries with this pattern
|
||||||
|
-random
|
||||||
|
generate a random password for a new entry or an update
|
||||||
-special
|
-special
|
||||||
use special chars to generate a random password
|
use special chars to generate a random password
|
||||||
|
-update
|
||||||
|
update an entry
|
||||||
-wallet string
|
-wallet string
|
||||||
specify the wallet
|
specify the wallet
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
4
cmd/gpm/go.mod
Normal file
4
cmd/gpm/go.mod
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module git.yaegashi.fr/nishiki/gpm/cmd/gpm
|
||||||
|
|
||||||
|
require git.yaegashi.fr/nishiki/gpm/gpm v0.0.0
|
||||||
|
replace git.yaegashi.fr/nishiki/gpm/gpm => ../../gpm
|
|
@ -15,7 +15,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.waks.be/nishiki/gpm/internal/gpm"
|
"git.yaegashi.fr/nishiki/gpm/gpm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
17
go.mod
17
go.mod
|
@ -1,17 +0,0 @@
|
||||||
module code.waks.be/nishiki/gpm
|
|
||||||
|
|
||||||
go 1.23.1
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/atotto/clipboard v0.1.4
|
|
||||||
github.com/gizak/termui/v3 v3.1.0
|
|
||||||
github.com/pquerna/otp v1.4.0
|
|
||||||
golang.org/x/crypto v0.28.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.2 // indirect
|
|
||||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
|
||||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
|
|
||||||
)
|
|
23
go.sum
23
go.sum
|
@ -1,23 +0,0 @@
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
|
|
||||||
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
|
|
||||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
|
||||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
|
||||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
|
||||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
|
|
||||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
|
||||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
|
332
gpm/cli.go
Normal file
332
gpm/cli.go
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
// 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 (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"github.com/atotto/clipboard"
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cli contain config and wallet to use
|
||||||
|
type Cli struct {
|
||||||
|
Config Config
|
||||||
|
Wallet Wallet
|
||||||
|
}
|
||||||
|
|
||||||
|
// printEntries show entries with tables
|
||||||
|
func (c *Cli) printEntries(entries []Entry) {
|
||||||
|
var otp string
|
||||||
|
var tables map[string]*tablewriter.Table
|
||||||
|
|
||||||
|
tables = make(map[string]*tablewriter.Table)
|
||||||
|
|
||||||
|
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) error(msg string) {
|
||||||
|
fmt.Printf("ERROR: %s\n", msg)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// input from the console
|
||||||
|
func (c *Cli) input(text string, defaultValue string, show bool) string {
|
||||||
|
fmt.Print(text)
|
||||||
|
|
||||||
|
if show == false {
|
||||||
|
data, _ := terminal.ReadPassword(int(syscall.Stdin))
|
||||||
|
text := string(data)
|
||||||
|
fmt.Printf("\n")
|
||||||
|
|
||||||
|
if text == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
input := bufio.NewScanner(os.Stdin)
|
||||||
|
input.Scan()
|
||||||
|
if input.Text() == "" {
|
||||||
|
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 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("your choice is not an integer or is out of range")
|
||||||
|
}
|
||||||
|
c1 <- index
|
||||||
|
}(len(entries))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case res := <-c1:
|
||||||
|
index = res
|
||||||
|
case <-time.After(30 * time.Second):
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadWallet get and unlock the wallet
|
||||||
|
func (c *Cli) loadWallet() {
|
||||||
|
var walletName string
|
||||||
|
|
||||||
|
passphrase := c.input("Enter the passphrase to unlock the wallet: ", "", false)
|
||||||
|
|
||||||
|
if *WALLET == "" {
|
||||||
|
walletName = c.Config.WalletDefault
|
||||||
|
} else {
|
||||||
|
walletName = *WALLET
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Wallet = Wallet{
|
||||||
|
Name: walletName,
|
||||||
|
Path: fmt.Sprintf("%s/%s.gpm", c.Config.WalletDir, walletName),
|
||||||
|
Passphrase: passphrase,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Wallet.Load()
|
||||||
|
if err != nil {
|
||||||
|
c.error(fmt.Sprintf("%s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the entry of a wallet
|
||||||
|
func (c *Cli) listEntry() {
|
||||||
|
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) deleteEntry() {
|
||||||
|
var entry Entry
|
||||||
|
|
||||||
|
c.loadWallet()
|
||||||
|
entry = c.selectEntry()
|
||||||
|
confirm := c.input("are you sure you want to remove this entry [y/N] ?", "N", true)
|
||||||
|
|
||||||
|
if confirm == "y" {
|
||||||
|
err := c.Wallet.DeleteEntry(entry.ID)
|
||||||
|
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 deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new entry in wallet
|
||||||
|
func (c *Cli) addEntry() {
|
||||||
|
c.loadWallet()
|
||||||
|
|
||||||
|
entry := Entry{}
|
||||||
|
entry.GenerateID()
|
||||||
|
entry.Name = c.input("Enter the name: ", "", true)
|
||||||
|
entry.Group = c.input("Enter the group: ", "", true)
|
||||||
|
entry.URI = c.input("Enter the URI: ", "", true)
|
||||||
|
entry.User = c.input("Enter the username: ", "", 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 OTP key: ", "", false)
|
||||||
|
entry.Comment = c.input("Enter a comment: ", "", true)
|
||||||
|
|
||||||
|
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 (c *Cli) ImportWallet() {
|
||||||
|
c.loadWallet()
|
||||||
|
|
||||||
|
_, err := os.Stat(*IMPORT)
|
||||||
|
if err != nil {
|
||||||
|
c.error(fmt.Sprintf("%s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(*IMPORT)
|
||||||
|
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")
|
||||||
|
}
|
1
gpm/go.mod
Normal file
1
gpm/go.mod
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module git.yaegashi.fr/nishiki/gpm
|
71
gpm/main.go
Normal file
71
gpm/main.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,35 +103,13 @@ func (w *Wallet) Save() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups return array with the groups name
|
|
||||||
func (w *Wallet) Groups() []string {
|
|
||||||
var groups []string
|
|
||||||
var exist bool
|
|
||||||
|
|
||||||
for _, entry := range w.Entries {
|
|
||||||
exist = false
|
|
||||||
for _, group := range groups {
|
|
||||||
if group == entry.Group {
|
|
||||||
exist = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if exist == false && entry.Group != "" {
|
|
||||||
groups = append(groups, entry.Group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchEntry return an array with the array expected with the pattern
|
// SearchEntry return an array with the array expected with the pattern
|
||||||
func (w *Wallet) SearchEntry(pattern string, group string, noGroup bool) []Entry {
|
func (w *Wallet) SearchEntry(pattern string, group string) []Entry {
|
||||||
var entries []Entry
|
var entries []Entry
|
||||||
r := regexp.MustCompile(strings.ToLower(pattern))
|
r := regexp.MustCompile(strings.ToLower(pattern))
|
||||||
|
|
||||||
for _, entry := range w.Entries {
|
for _, entry := range w.Entries {
|
||||||
if (noGroup && entry.Group != "") || (!noGroup && group != "" && strings.ToLower(entry.Group) != strings.ToLower(group)) {
|
if group != "" && strings.ToLower(entry.Group) != strings.ToLower(group) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r.Match([]byte(strings.ToLower(entry.Name))) ||
|
if r.Match([]byte(strings.ToLower(entry.Name))) ||
|
|
@ -62,12 +62,12 @@ func TestSearchEntryWithGoodID(t *testing.T) {
|
||||||
|
|
||||||
func TestSearchEntriesByGroup(t *testing.T) {
|
func TestSearchEntriesByGroup(t *testing.T) {
|
||||||
wallet := generateWalletWithEntries()
|
wallet := generateWalletWithEntries()
|
||||||
entries := len(wallet.SearchEntry("", "BAD-GROUP", false))
|
entries := len(wallet.SearchEntry("", "BAD-GROUP"))
|
||||||
if entries != 0 {
|
if entries != 0 {
|
||||||
t.Errorf("a search with bad group must return 0 entry: %d", entries)
|
t.Errorf("a search with bad group must return 0 entry: %d", entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries = len(wallet.SearchEntry("", "good group", false))
|
entries = len(wallet.SearchEntry("", "good group"))
|
||||||
if entries != 10 {
|
if entries != 10 {
|
||||||
t.Errorf("a search with good group must return 10 entries: %d", entries)
|
t.Errorf("a search with good group must return 10 entries: %d", entries)
|
||||||
}
|
}
|
||||||
|
@ -75,17 +75,17 @@ func TestSearchEntriesByGroup(t *testing.T) {
|
||||||
|
|
||||||
func TestSearchEntriesByPattern(t *testing.T) {
|
func TestSearchEntriesByPattern(t *testing.T) {
|
||||||
wallet := generateWalletWithEntries()
|
wallet := generateWalletWithEntries()
|
||||||
entries := len(wallet.SearchEntry("BAD-PATTERN", "", false))
|
entries := len(wallet.SearchEntry("BAD-PATTERN", ""))
|
||||||
if entries != 0 {
|
if entries != 0 {
|
||||||
t.Errorf("a search with bad pattern must return 0 entry: %d", entries)
|
t.Errorf("a search with bad pattern must return 0 entry: %d", entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries = len(wallet.SearchEntry("entry", "", false))
|
entries = len(wallet.SearchEntry("entry", ""))
|
||||||
if entries != 10 {
|
if entries != 10 {
|
||||||
t.Errorf("a search with good pattern must return 10 entries: %d", entries)
|
t.Errorf("a search with good pattern must return 10 entries: %d", entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries = len(wallet.SearchEntry("^entry 5$", "", false))
|
entries = len(wallet.SearchEntry("^entry 5$", ""))
|
||||||
if entries != 1 {
|
if entries != 1 {
|
||||||
t.Errorf("a search with specific pattern must return 1 entry: %d", entries)
|
t.Errorf("a search with specific pattern must return 1 entry: %d", entries)
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ func TestSearchEntriesByPattern(t *testing.T) {
|
||||||
|
|
||||||
func TestSearchEntriesByPatternAndGroup(t *testing.T) {
|
func TestSearchEntriesByPatternAndGroup(t *testing.T) {
|
||||||
wallet := generateWalletWithEntries()
|
wallet := generateWalletWithEntries()
|
||||||
entries := len(wallet.SearchEntry("entry", "good group", false))
|
entries := len(wallet.SearchEntry("entry", "good group"))
|
||||||
if entries != 10 {
|
if entries != 10 {
|
||||||
t.Errorf("a search with good pattern and godd group must return 10 entries: %d", entries)
|
t.Errorf("a search with good pattern and godd group must return 10 entries: %d", entries)
|
||||||
}
|
}
|
||||||
|
@ -233,14 +233,3 @@ func TestLoadWalletWithBadPassword(t *testing.T) {
|
||||||
t.Errorf("must have 0 entries: %d", entries)
|
t.Errorf("must have 0 entries: %d", entries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetGroup(t *testing.T) {
|
|
||||||
wallet := generateWalletWithEntries()
|
|
||||||
groups := wallet.Groups()
|
|
||||||
if len(groups) != 1 {
|
|
||||||
t.Errorf("there must have 1 group: %d", len(groups))
|
|
||||||
}
|
|
||||||
if groups[0] != "Good Group" {
|
|
||||||
t.Errorf("the group name isn't 'Good Group': %s", groups[0])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,565 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SelectBox to select an item from a list
|
|
||||||
func (c *Cli) SelectBox(title string, items []string) string {
|
|
||||||
l := widgets.NewList()
|
|
||||||
l.Title = title
|
|
||||||
l.TextStyle = ui.NewStyle(ui.ColorYellow)
|
|
||||||
l.SelectedRowStyle = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierBold)
|
|
||||||
l.WrapText = false
|
|
||||||
l.SetRect(10, 10, 70, 5)
|
|
||||||
l.Rows = items
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupsBox 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(25, 0, 80, 20)
|
|
||||||
l.Rows = append(c.Wallet.Groups(), "No group")
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
}
|
|
||||||
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 cuts"
|
|
||||||
p.Text = `[<escape>](fg:yellow) clear current search
|
|
||||||
[<enter> ](fg:yellow) select entry or group
|
|
||||||
[<up> ](fg:yellow) move cursor up
|
|
||||||
[<down> ](fg:yellow) move cursor 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)
|
|
||||||
if entry.Group == "" || c.ChoiceBox("Change the group ?", false) {
|
|
||||||
group := c.SelectBox("Group", append(c.Wallet.Groups(), "* Create new group *"))
|
|
||||||
if group == "* Create new group *" || group == "" {
|
|
||||||
entry.Group = c.InputBox("Group", "", false)
|
|
||||||
} else {
|
|
||||||
entry.Group = group
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
} else {
|
|
||||||
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)
|
|
||||||
group := c.SelectBox("Group", append(c.Wallet.Groups(), "* Create new group *"))
|
|
||||||
if group == "* Create new group *" || group == "" {
|
|
||||||
entry.Group = c.InputBox("Group", "", false)
|
|
||||||
} else {
|
|
||||||
entry.Group = group
|
|
||||||
}
|
|
||||||
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
|
|
||||||
noGroup := false
|
|
||||||
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 if noGroup {
|
|
||||||
l.Title = "Group: No group"
|
|
||||||
} else {
|
|
||||||
l.Title = "Group: All"
|
|
||||||
}
|
|
||||||
|
|
||||||
if refresh {
|
|
||||||
refresh = false
|
|
||||||
index = -1
|
|
||||||
entries = c.Wallet.SearchEntry(pattern, group, noGroup)
|
|
||||||
l.Rows = []string{}
|
|
||||||
for _, entry := range entries {
|
|
||||||
l.Rows = append(l.Rows, entry.Name)
|
|
||||||
}
|
|
||||||
ui.Clear()
|
|
||||||
c.NotificationBox("press h to view short cuts", 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()
|
|
||||||
index = -1
|
|
||||||
case "q":
|
|
||||||
clipboard.WriteAll("")
|
|
||||||
ch <- true
|
|
||||||
case "<Enter>":
|
|
||||||
index = l.SelectedRow
|
|
||||||
case "<Escape>":
|
|
||||||
pattern = ""
|
|
||||||
group = "all"
|
|
||||||
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()
|
|
||||||
noGroup = false
|
|
||||||
if group == "No group" {
|
|
||||||
group = ""
|
|
||||||
noGroup = true
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
c.NotificationBox("the username is copied in clipboard", false)
|
|
||||||
}
|
|
||||||
case "<C-c>":
|
|
||||||
if selected {
|
|
||||||
c.NotificationBox("the password is copied in clipboard", false)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uiEvents = ui.PollEvents()
|
|
||||||
ch <- false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportWallet 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportWallet 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue