style: change space to tab for goreport

This commit is contained in:
Adrien Waksberg 2019-08-31 19:18:35 +02:00
parent 30ada1a1fc
commit 3f9d4850b6
13 changed files with 853 additions and 842 deletions

View file

@ -17,6 +17,7 @@ Which is based on [Keep A Changelog](http://keepachangelog.com/)
- Export in a file - Export in a file
- Use RandomString function for wallet's salt - Use RandomString function for wallet's salt
- Search is case insensite - Search is case insensite
- change space to tab for goreport
## v1.1.0 - 2019-07-23 ## v1.1.0 - 2019-07-23

View file

@ -1,8 +1,9 @@
# gpm: Go Passwords Manager # gpm: Go Passwords Manager
[![Version](https://img.shields.io/badge/latest_version-1.1.0-green.svg)](https://git.yaegashi.fr/nishiki/gpm/releases) [![Version](https://img.shields.io/badge/latest_version-1.1.0-green.svg)](https://git.yaegashi.fr/nishiki/gpm/releases)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://git.yaegashi.fr/nishiki/gpm/src/branch/master/LICENSE)
[![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/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://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

View file

@ -14,10 +14,10 @@
package main package main
import( import (
"git.yaegashi.fr/nishiki/gpm/gpm" "git.yaegashi.fr/nishiki/gpm/gpm"
) )
func main() { func main() {
gpm.Run() gpm.Run()
} }

View file

@ -14,294 +14,298 @@
package gpm package gpm
import( import (
"bufio" "bufio"
"fmt" "fmt"
"io/ioutil" "github.com/atotto/clipboard"
"os" "github.com/olekukonko/tablewriter"
"strconv" "golang.org/x/crypto/ssh/terminal"
"syscall" "io/ioutil"
"github.com/atotto/clipboard" "os"
"github.com/olekukonko/tablewriter" "strconv"
"golang.org/x/crypto/ssh/terminal" "syscall"
) )
// Cli contain config and wallet to use // 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 // printEntries show entries with tables
func (c *Cli) printEntries(entries []Entry) { func (c *Cli) printEntries(entries []Entry) {
var otp string var otp string
var tables map[string]*tablewriter.Table var tables map[string]*tablewriter.Table
tables = make(map[string]*tablewriter.Table) tables = make(map[string]*tablewriter.Table)
for i, entry := range entries { for i, entry := range entries {
if entry.OTP == "" { otp = "" } else { otp = "X" } if entry.OTP == "" {
if _, present := tables[entry.Group]; present == false { otp = ""
tables[entry.Group] = tablewriter.NewWriter(os.Stdout) } else {
tables[entry.Group].SetHeader([]string{"", "Name", "URI", "User", "OTP", "Comment"}) otp = "X"
tables[entry.Group].SetBorder(false) }
tables[entry.Group].SetColumnColor( if _, present := tables[entry.Group]; present == false {
tablewriter.Colors{tablewriter.Normal, tablewriter.FgYellowColor}, tables[entry.Group] = tablewriter.NewWriter(os.Stdout)
tablewriter.Colors{tablewriter.Normal, tablewriter.FgWhiteColor}, tables[entry.Group].SetHeader([]string{"", "Name", "URI", "User", "OTP", "Comment"})
tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor}, tables[entry.Group].SetBorder(false)
tablewriter.Colors{tablewriter.Normal, tablewriter.FgGreenColor}, tables[entry.Group].SetColumnColor(
tablewriter.Colors{tablewriter.Normal, tablewriter.FgWhiteColor}, tablewriter.Colors{tablewriter.Normal, tablewriter.FgYellowColor},
tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor}) 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 }) tables[entry.Group].Append([]string{strconv.Itoa(i), entry.Name, entry.URI, entry.User, otp, entry.Comment})
} }
for group, table := range tables { for group, table := range tables {
fmt.Printf("\n%s\n\n", group) fmt.Printf("\n%s\n\n", group)
table.Render() table.Render()
fmt.Println("") fmt.Println("")
} }
} }
// error print a message and exit) // error print a message and exit)
func (c *Cli) error(msg string) { func (c *Cli) error(msg string) {
fmt.Printf("ERROR: %s\n", msg) fmt.Printf("ERROR: %s\n", msg)
os.Exit(2) os.Exit(2)
} }
// input from the console // input from the console
func (c *Cli) input(text string, defaultValue string, show bool) string { func (c *Cli) input(text string, defaultValue string, show bool) string {
fmt.Print(text) fmt.Print(text)
if show == false { if show == false {
data, _ := terminal.ReadPassword(int(syscall.Stdin)) data, _ := terminal.ReadPassword(int(syscall.Stdin))
text := string(data) text := string(data)
fmt.Printf("\n") fmt.Printf("\n")
if text == "" { if text == "" {
return defaultValue return defaultValue
} }
return text return text
} }
input := bufio.NewScanner(os.Stdin) input := bufio.NewScanner(os.Stdin)
input.Scan() input.Scan()
if input.Text() == "" { if input.Text() == "" {
return defaultValue return defaultValue
} }
return input.Text() return input.Text()
} }
// selectEntry with a form // selectEntry with a form
func (c *Cli) selectEntry() Entry { func (c *Cli) selectEntry() Entry {
var index int var index int
entries := c.Wallet.SearchEntry(*PATTERN, *GROUP) entries := c.Wallet.SearchEntry(*PATTERN, *GROUP)
if len(entries) == 0 { if len(entries) == 0 {
fmt.Println("no entry found") fmt.Println("no entry found")
os.Exit(1) os.Exit(1)
} }
c.printEntries(entries) c.printEntries(entries)
if len(entries) == 1 { if len(entries) == 1 {
return entries[0] return entries[0]
} }
for true { for true {
index, err := strconv.Atoi(c.input("Select the entry: ", "", true)) index, err := strconv.Atoi(c.input("Select the entry: ", "", true))
if err == nil && index >= 0 && index + 1 <= len(entries) { if err == nil && index >= 0 && index+1 <= len(entries) {
break break
} }
fmt.Println("your choice is not an integer or is out of range") fmt.Println("your choice is not an integer or is out of range")
} }
return entries[index] return entries[index]
} }
// loadWallet get and unlock the wallet // loadWallet get and unlock the wallet
func (c *Cli) loadWallet() { func (c *Cli) loadWallet() {
var walletName string var walletName string
passphrase := c.input("Enter the passphrase to unlock the wallet: ", "", false) passphrase := c.input("Enter the passphrase to unlock the wallet: ", "", false)
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, Passphrase: passphrase,
} }
err := c.Wallet.Load() err := c.Wallet.Load()
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
} }
// List the entry of a wallet // List the entry of a wallet
func (c *Cli) listEntry() { func (c *Cli) listEntry() {
c.loadWallet() c.loadWallet()
entries := c.Wallet.SearchEntry(*PATTERN, *GROUP) entries := c.Wallet.SearchEntry(*PATTERN, *GROUP)
if len(entries) == 0 { if len(entries) == 0 {
fmt.Println("no entry found") fmt.Println("no entry found")
os.Exit(1) os.Exit(1)
} else { } else {
c.printEntries(entries) c.printEntries(entries)
} }
} }
// Delete an entry of a wallet // Delete an entry of a wallet
func (c *Cli) deleteEntry() { func (c *Cli) deleteEntry() {
var entry Entry var entry Entry
c.loadWallet() c.loadWallet()
entry = c.selectEntry() entry = c.selectEntry()
confirm := c.input("are you sure you want to remove this entry [y/N] ?", "N", true) confirm := c.input("are you sure you want to remove this entry [y/N] ?", "N", true)
if confirm == "y" { if confirm == "y" {
err := c.Wallet.DeleteEntry(entry.ID) err := c.Wallet.DeleteEntry(entry.ID)
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
err = c.Wallet.Save() err = c.Wallet.Save()
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
fmt.Println("the entry has been deleted") fmt.Println("the entry has been deleted")
} }
} }
// Add a new entry in wallet // Add a new entry in wallet
func (c *Cli) addEntry() { func (c *Cli) addEntry() {
c.loadWallet() c.loadWallet()
entry := Entry{} entry := Entry{}
entry.GenerateID() entry.GenerateID()
entry.Name = c.input("Enter the name: ", "", true) entry.Name = c.input("Enter the name: ", "", true)
entry.Group = c.input("Enter the group: ", "", true) entry.Group = c.input("Enter the group: ", "", true)
entry.URI = c.input("Enter the URI: ", "", true) entry.URI = c.input("Enter the URI: ", "", true)
entry.User = c.input("Enter the username: ", "", true) entry.User = c.input("Enter the username: ", "", true)
if *RANDOM { if *RANDOM {
entry.Password = RandomString(c.Config.PasswordLength, entry.Password = RandomString(c.Config.PasswordLength,
c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial) c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial)
} else { } else {
entry.Password = c.input("Enter the new password: ", entry.Password, false) entry.Password = c.input("Enter the new password: ", entry.Password, false)
} }
entry.OTP = c.input("Enter the OTP key: ", "", false) entry.OTP = c.input("Enter the OTP key: ", "", false)
entry.Comment = c.input("Enter a comment: ", "", true) entry.Comment = c.input("Enter a comment: ", "", true)
err := c.Wallet.AddEntry(entry) err := c.Wallet.AddEntry(entry)
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
err = c.Wallet.Save() err = c.Wallet.Save()
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
fmt.Println("the entry has been added") fmt.Println("the entry has been added")
} }
// Update an entry in wallet // Update an entry in wallet
func (c *Cli) updateEntry() { func (c *Cli) updateEntry() {
c.loadWallet() c.loadWallet()
entry := c.selectEntry() entry := c.selectEntry()
entry.Name = c.input("Enter the new name: ", entry.Name, true) entry.Name = c.input("Enter the new name: ", entry.Name, true)
entry.Group = c.input("Enter the new group: ", entry.Group, true) entry.Group = c.input("Enter the new group: ", entry.Group, true)
entry.URI = c.input("Enter the new URI: ", entry.URI, true) entry.URI = c.input("Enter the new URI: ", entry.URI, true)
entry.User = c.input("Enter the new username: ", entry.User, true) entry.User = c.input("Enter the new username: ", entry.User, true)
if *RANDOM { if *RANDOM {
entry.Password = RandomString(c.Config.PasswordLength, entry.Password = RandomString(c.Config.PasswordLength,
c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial) c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial)
} else { } else {
entry.Password = c.input("Enter the new password: ", entry.Password, false) entry.Password = c.input("Enter the new password: ", entry.Password, false)
} }
entry.OTP = c.input("Enter the new OTP key: ", entry.OTP, false) entry.OTP = c.input("Enter the new OTP key: ", entry.OTP, false)
entry.Comment = c.input("Enter a new comment: ", entry.Comment, true) entry.Comment = c.input("Enter a new comment: ", entry.Comment, true)
err := c.Wallet.UpdateEntry(entry) err := c.Wallet.UpdateEntry(entry)
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
c.Wallet.Save() c.Wallet.Save()
} }
// Copy login and password from an entry // Copy login and password from an entry
func (c *Cli) copyEntry() { func (c *Cli) copyEntry() {
c.loadWallet() c.loadWallet()
entry := c.selectEntry() entry := c.selectEntry()
for true { for true {
choice := c.input("select one action: ", "", true) choice := c.input("select one action: ", "", true)
switch choice { switch choice {
case "l": case "l":
clipboard.WriteAll(entry.User) clipboard.WriteAll(entry.User)
case "p": case "p":
clipboard.WriteAll(entry.Password) clipboard.WriteAll(entry.Password)
case "o": case "o":
code, time, _ := entry.OTPCode() code, time, _ := entry.OTPCode()
fmt.Printf("this OTP code is available for %d seconds\n", time) fmt.Printf("this OTP code is available for %d seconds\n", time)
clipboard.WriteAll(code) clipboard.WriteAll(code)
case "q": case "q":
os.Exit(0) os.Exit(0)
default: default:
fmt.Println("l -> copy login") fmt.Println("l -> copy login")
fmt.Println("p -> copy password") fmt.Println("p -> copy password")
fmt.Println("o -> copy OTP code") fmt.Println("o -> copy OTP code")
fmt.Println("q -> quit") fmt.Println("q -> quit")
} }
} }
} }
// Import entries from json file // Import entries from json file
func (c *Cli) ImportWallet() { func (c *Cli) ImportWallet() {
c.loadWallet() c.loadWallet()
_, err := os.Stat(*IMPORT) _, err := os.Stat(*IMPORT)
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
data, err := ioutil.ReadFile(*IMPORT) data, err := ioutil.ReadFile(*IMPORT)
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
err = c.Wallet.Import(data) err = c.Wallet.Import(data)
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
err = c.Wallet.Save() err = c.Wallet.Save()
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
fmt.Println("the import was successful") fmt.Println("the import was successful")
} }
// Export a wallet in json format // Export a wallet in json format
func (c *Cli) ExportWallet() { func (c *Cli) ExportWallet() {
c.loadWallet() c.loadWallet()
data, err := c.Wallet.Export() data, err := c.Wallet.Export()
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
err = ioutil.WriteFile(*EXPORT, data, 0600) err = ioutil.WriteFile(*EXPORT, data, 0600)
if err != nil { if err != nil {
c.error(fmt.Sprintf("%s", err)) c.error(fmt.Sprintf("%s", err))
} }
fmt.Println("the export was successful") fmt.Println("the export was successful")
} }

View file

@ -15,90 +15,90 @@
package gpm package gpm
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/user" "os/user"
"runtime" "runtime"
) )
// Config struct contain the config // Config struct contain the config
type Config struct { type Config struct {
WalletDir string `json:"wallet_dir"` WalletDir string `json:"wallet_dir"`
WalletDefault string `json:"wallet_default"` WalletDefault string `json:"wallet_default"`
PasswordLength int `json:"password_length"` PasswordLength int `json:"password_length"`
PasswordLetter bool `json:"password_letter"` PasswordLetter bool `json:"password_letter"`
PasswordDigit bool `json:"password_digit"` PasswordDigit bool `json:"password_digit"`
PasswordSpecial bool `json:"password_special"` PasswordSpecial bool `json:"password_special"`
} }
// Init the configuration // Init the configuration
func (c *Config) Init() error { func (c *Config) Init() error {
user, err := user.Current() user, err := user.Current()
if err != nil { if err != nil {
return err return err
} }
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
c.WalletDir = fmt.Sprintf("%s/Library/Preferences/gpm", user.HomeDir) c.WalletDir = fmt.Sprintf("%s/Library/Preferences/gpm", user.HomeDir)
} else if runtime.GOOS == "windows" { } else if runtime.GOOS == "windows" {
c.WalletDir = fmt.Sprintf("%s/AppData/Local/gpm", user.HomeDir) c.WalletDir = fmt.Sprintf("%s/AppData/Local/gpm", user.HomeDir)
} else { } else {
c.WalletDir = fmt.Sprintf("%s/.config/gpm", user.HomeDir) c.WalletDir = fmt.Sprintf("%s/.config/gpm", user.HomeDir)
} }
c.WalletDefault = "default" c.WalletDefault = "default"
c.PasswordLength = 16 c.PasswordLength = 16
c.PasswordLetter = true c.PasswordLetter = true
c.PasswordDigit = true c.PasswordDigit = true
c.PasswordSpecial = false c.PasswordSpecial = false
return nil return nil
} }
// Load the configuration from a file // Load the configuration from a file
func (c *Config) Load(path string) error { func (c *Config) Load(path string) error {
err := c.Init() err := c.Init()
if err != nil { if err != nil {
return err return err
} }
if path != "" { if path != "" {
_, err = os.Stat(path) _, err = os.Stat(path)
if err != nil { if err != nil {
} }
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }
err = json.Unmarshal(data, &c) err = json.Unmarshal(data, &c)
if err != nil { if err != nil {
return err return err
} }
} }
err = os.MkdirAll(c.WalletDir, 0700) err = os.MkdirAll(c.WalletDir, 0700)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
// Save the configuration // Save the configuration
func (c *Config) Save(path string) error { func (c *Config) Save(path string) error {
data, err := json.Marshal(&c) data, err := json.Marshal(&c)
if err != nil { if err != nil {
return err return err
} }
err = ioutil.WriteFile(path, []byte(data), 0644) err = ioutil.WriteFile(path, []byte(data), 0644)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }

View file

@ -1,72 +1,72 @@
package gpm package gpm
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
) )
func TestInit(t *testing.T) { func TestInit(t *testing.T) {
var config Config var config Config
err := config.Init() err := config.Init()
if err != nil { if err != nil {
t.Error("the config init mustn't return an error") t.Error("the config init mustn't return an error")
} }
if config.WalletDefault != "default" { if config.WalletDefault != "default" {
t.Errorf("the WalletDefaut must be 'default': %s", config.WalletDefault) t.Errorf("the WalletDefaut must be 'default': %s", config.WalletDefault)
} }
if config.PasswordLength != 16 { if config.PasswordLength != 16 {
t.Errorf("the PasswordLength must be 16: %d", config.PasswordLength) t.Errorf("the PasswordLength must be 16: %d", config.PasswordLength)
} }
if config.PasswordLetter != true { if config.PasswordLetter != true {
t.Error("the PasswordLetter must be true") t.Error("the PasswordLetter must be true")
} }
if config.PasswordDigit != true { if config.PasswordDigit != true {
t.Error("the PasswordDigit must be true") t.Error("the PasswordDigit must be true")
} }
if config.PasswordSpecial != false { if config.PasswordSpecial != false {
t.Error("the PasswordSpecial must be false") t.Error("the PasswordSpecial must be false")
} }
} }
func TestSave(t *testing.T) { func TestSave(t *testing.T) {
var config Config var config Config
tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-") tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-")
defer os.Remove(tmpFile.Name()) defer os.Remove(tmpFile.Name())
config.Init() config.Init()
err := config.Save(tmpFile.Name()) err := config.Save(tmpFile.Name())
if err != nil { if err != nil {
t.Errorf("save config mustn't return an error: %s", err) t.Errorf("save config mustn't return an error: %s", err)
} }
} }
func TestLoadWithFile(t *testing.T) { func TestLoadWithFile(t *testing.T) {
var config Config var config Config
tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-") tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-")
defer os.Remove(tmpFile.Name()) defer os.Remove(tmpFile.Name())
config.Init() config.Init()
config.Save(tmpFile.Name()) config.Save(tmpFile.Name())
err := config.Load(tmpFile.Name()) err := config.Load(tmpFile.Name())
if err != nil { if err != nil {
t.Errorf("load config with file mustn't return an error: %s", err) t.Errorf("load config with file mustn't return an error: %s", err)
} }
} }
func TestLoadWithoutFile(t *testing.T) { func TestLoadWithoutFile(t *testing.T) {
var config Config var config Config
err := config.Load("") err := config.Load("")
if err != nil { if err != nil {
t.Errorf("load config without file mustn't return an error: %s", err) t.Errorf("load config without file mustn't return an error: %s", err)
} }
} }

View file

@ -14,71 +14,71 @@
package gpm package gpm
import( import (
"crypto/aes" "crypto/aes"
"crypto/sha512" "crypto/cipher"
"crypto/cipher" "crypto/rand"
"crypto/rand" "crypto/sha512"
"encoding/base64" "encoding/base64"
"io" "io"
mrand "math/rand" mrand "math/rand"
"time" "time"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
// Encrypt data with aes256 // Encrypt data with aes256
func Encrypt(data []byte, passphrase string, salt string) (string, error) { func Encrypt(data []byte, passphrase string, salt string) (string, error) {
key := pbkdf2.Key([]byte(passphrase), []byte(salt), 4096, 32, sha512.New) key := pbkdf2.Key([]byte(passphrase), []byte(salt), 4096, 32, sha512.New)
block, err := aes.NewCipher([]byte(key)) block, err := aes.NewCipher([]byte(key))
if err != nil { if err != nil {
return "", err return "", err
} }
cipher, err := cipher.NewGCM(block) cipher, err := cipher.NewGCM(block)
if err != nil { if err != nil {
return "", err return "", err
} }
nonce := make([]byte, cipher.NonceSize()) nonce := make([]byte, cipher.NonceSize())
_, err = io.ReadFull(rand.Reader, nonce) _, err = io.ReadFull(rand.Reader, nonce)
if err != nil { if err != nil {
return "", err return "", err
} }
dataEncrypted := cipher.Seal(nonce, nonce, data, nil) dataEncrypted := cipher.Seal(nonce, nonce, data, nil)
return base64.StdEncoding.EncodeToString(dataEncrypted), nil return base64.StdEncoding.EncodeToString(dataEncrypted), nil
} }
// Decrypt data // Decrypt data
func Decrypt(data string, passphrase string, salt string) ([]byte, error) { func Decrypt(data string, passphrase string, salt string) ([]byte, error) {
key := pbkdf2.Key([]byte(passphrase), []byte(salt), 4096, 32, sha512.New) key := pbkdf2.Key([]byte(passphrase), []byte(salt), 4096, 32, sha512.New)
rawData, err := base64.StdEncoding.DecodeString(data) rawData, err := base64.StdEncoding.DecodeString(data)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
} }
block, err := aes.NewCipher([]byte(key)) block, err := aes.NewCipher([]byte(key))
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
} }
cipher, err := cipher.NewGCM(block) cipher, err := cipher.NewGCM(block)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
} }
nonceSize := cipher.NonceSize() nonceSize := cipher.NonceSize()
nonce, text := rawData[:nonceSize], rawData[nonceSize:] nonce, text := rawData[:nonceSize], rawData[nonceSize:]
plaintext, err := cipher.Open(nil, nonce, text, nil) plaintext, err := cipher.Open(nil, nonce, text, nil)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
} }
return plaintext, nil return plaintext, nil
} }
// RandomString generate a random string // RandomString generate a random string
@ -86,20 +86,26 @@ func RandomString(length int, letter bool, digit bool, special bool) string {
digits := "0123456789" digits := "0123456789"
specials := "~=+%^*/()[]{}/!@#$?|" specials := "~=+%^*/()[]{}/!@#$?|"
letters := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" letters := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
chars := "" chars := ""
randomString := make([]byte, length) randomString := make([]byte, length)
if letter { chars = chars + letters } if letter {
if digit { chars = chars + digits } chars = chars + letters
if special { chars = chars + specials } }
if !letter && !digit && !special { if digit {
chars = digits + letters chars = chars + digits
} }
if special {
mrand.Seed(time.Now().UnixNano()) chars = chars + specials
for i := 0; i < length; i++ { }
randomString[i] = chars[mrand.Intn(len(chars))] if !letter && !digit && !special {
chars = digits + letters
} }
return string(randomString) mrand.Seed(time.Now().UnixNano())
for i := 0; i < length; i++ {
randomString[i] = chars[mrand.Intn(len(chars))]
}
return string(randomString)
} }

View file

@ -1,91 +1,91 @@
package gpm package gpm
import ( import (
"regexp" "regexp"
"testing" "testing"
) )
func TestEncrypt(t *testing.T) { func TestEncrypt(t *testing.T) {
secret := []byte("secret data") secret := []byte("secret data")
data, err := Encrypt(secret, "passphrase", "salt") data, err := Encrypt(secret, "passphrase", "salt")
if err != nil { if err != nil {
t.Errorf("Encrypt mustn't return an error: %s", err) t.Errorf("Encrypt mustn't return an error: %s", err)
} }
if data == "" { if data == "" {
t.Error("Encrypt must generate a string not empty") t.Error("Encrypt must generate a string not empty")
} }
} }
func TestDecrypt(t *testing.T) { func TestDecrypt(t *testing.T) {
secret := "secret data" secret := "secret data"
dataEncrypted, _ := Encrypt([]byte(secret), "passphrase", "salt") dataEncrypted, _ := Encrypt([]byte(secret), "passphrase", "salt")
data, err := Decrypt(dataEncrypted, "passphrase", "salt") data, err := Decrypt(dataEncrypted, "passphrase", "salt")
if err != nil { if err != nil {
t.Errorf("Decrypt mustn't return an error: %s", err) t.Errorf("Decrypt mustn't return an error: %s", err)
} }
if string(data) != secret { if string(data) != secret {
t.Errorf("the encrypted secret is different of decrypted secret: %s", data) t.Errorf("the encrypted secret is different of decrypted secret: %s", data)
} }
} }
func TestDecryptWithBadPassphrase(t *testing.T) { func TestDecryptWithBadPassphrase(t *testing.T) {
secret := []byte("secret data") secret := []byte("secret data")
dataEncrypted, _ := Encrypt(secret, "passphrase", "salt") dataEncrypted, _ := Encrypt(secret, "passphrase", "salt")
_, err := Decrypt(dataEncrypted, "bad", "salt") _, err := Decrypt(dataEncrypted, "bad", "salt")
if err == nil { if err == nil {
t.Error("Decrypt must return an error with bad passphrase") t.Error("Decrypt must return an error with bad passphrase")
} }
} }
func TestDecryptWithBadSalt(t *testing.T) { func TestDecryptWithBadSalt(t *testing.T) {
secret := []byte("secret data") secret := []byte("secret data")
dataEncrypted, _ := Encrypt(secret, "passphrase", "salt") dataEncrypted, _ := Encrypt(secret, "passphrase", "salt")
_, err := Decrypt(dataEncrypted, "passphrase", "bad") _, err := Decrypt(dataEncrypted, "passphrase", "bad")
if err == nil { if err == nil {
t.Error("Decrypt must return an error with bad salt") t.Error("Decrypt must return an error with bad salt")
} }
} }
func TestRandomStringLength(t *testing.T) { func TestRandomStringLength(t *testing.T) {
password := RandomString(64, false, false, false) password := RandomString(64, false, false, false)
if len(password) != 64 { if len(password) != 64 {
t.Errorf("the string must have 64 chars: %d", len(password)) t.Errorf("the string must have 64 chars: %d", len(password))
} }
r := regexp.MustCompile(`^[a-zA-Z0-9]{64}$`) r := regexp.MustCompile(`^[a-zA-Z0-9]{64}$`)
match := r.FindSubmatch([]byte(password)) match := r.FindSubmatch([]byte(password))
if len(match) == 0 { if len(match) == 0 {
t.Errorf("the string must contain only digit and alphabetic characters: %s", password) t.Errorf("the string must contain only digit and alphabetic characters: %s", password)
} }
} }
func TestRandomStringOnlyDigit(t *testing.T) { func TestRandomStringOnlyDigit(t *testing.T) {
password := RandomString(64, false, true, false) password := RandomString(64, false, true, false)
r := regexp.MustCompile(`^[0-9]{64}$`) r := regexp.MustCompile(`^[0-9]{64}$`)
match := r.FindSubmatch([]byte(password)) match := r.FindSubmatch([]byte(password))
if len(match) == 0 { if len(match) == 0 {
t.Errorf("the string must contain only digit characters: %s", password) t.Errorf("the string must contain only digit characters: %s", password)
} }
} }
func TestRandomStringOnlyAlphabetic(t *testing.T) { func TestRandomStringOnlyAlphabetic(t *testing.T) {
password := RandomString(64, true, false, false) password := RandomString(64, true, false, false)
r := regexp.MustCompile(`^[a-zA-Z]{64}$`) r := regexp.MustCompile(`^[a-zA-Z]{64}$`)
match := r.FindSubmatch([]byte(password)) match := r.FindSubmatch([]byte(password))
if len(match) == 0 { if len(match) == 0 {
t.Errorf("the string must contain only alphabetic characters: %s", password) t.Errorf("the string must contain only alphabetic characters: %s", password)
} }
} }
func TestRandomStringOnlySpecial(t *testing.T) { func TestRandomStringOnlySpecial(t *testing.T) {
password := RandomString(64, false, false, true) password := RandomString(64, false, false, true)
r := regexp.MustCompile(`^[\~\=\+\%\^\*\/\(\)\[\]\{\}\!\@\#\$\?\|]{64}$`) r := regexp.MustCompile(`^[\~\=\+\%\^\*\/\(\)\[\]\{\}\!\@\#\$\?\|]{64}$`)
match := r.FindSubmatch([]byte(password)) match := r.FindSubmatch([]byte(password))
if len(match) == 0 { if len(match) == 0 {
t.Errorf("the string must contain only alphabetic characters: %s", password) t.Errorf("the string must contain only alphabetic characters: %s", password)
} }
} }

View file

@ -14,58 +14,58 @@
package gpm package gpm
import( import (
"fmt" "fmt"
"time" "net/url"
"net/url" "time"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
) )
// Entry struct have the password informations // Entry struct have the password informations
type Entry struct { type Entry struct {
Name string Name string
ID string ID string
URI string URI string
User string User string
Password string Password string
OTP string OTP string
Group string Group string
Comment string Comment string
Create int64 Create int64
LastUpdate int64 LastUpdate int64
} }
// Verify if the item have'nt error // Verify if the item have'nt error
func (e *Entry) Verify() error { func (e *Entry) Verify() error {
if e.ID == "" { if e.ID == "" {
return fmt.Errorf("you must generate an ID") return fmt.Errorf("you must generate an ID")
} }
if e.Name == "" { if e.Name == "" {
return fmt.Errorf("you must define a name") return fmt.Errorf("you must define a name")
} }
uri, _ := url.Parse(e.URI) uri, _ := url.Parse(e.URI)
if e.URI != "" && uri.Host == "" { if e.URI != "" && uri.Host == "" {
return fmt.Errorf("the uri isn't a valid uri") return fmt.Errorf("the uri isn't a valid uri")
} }
return nil return nil
} }
// GenerateID create a new id for the entry // GenerateID create a new id for the entry
func (e *Entry) GenerateID() { func (e *Entry) GenerateID() {
e.ID = fmt.Sprintf("%d", time.Now().UnixNano()) e.ID = fmt.Sprintf("%d", time.Now().UnixNano())
} }
// OTPCode generate an OTP Code // OTPCode generate an OTP Code
func (e *Entry) OTPCode() (string, int64, error){ func (e *Entry) OTPCode() (string, int64, error) {
code, err := totp.GenerateCode(e.OTP, time.Now()) code, err := totp.GenerateCode(e.OTP, time.Now())
time := 30 - (time.Now().Unix() % 30) time := 30 - (time.Now().Unix() % 30)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
return code, time, nil return code, time, nil
} }

View file

@ -3,63 +3,63 @@ package gpm
import "testing" import "testing"
func TestCreateEmptyEntry(t *testing.T) { func TestCreateEmptyEntry(t *testing.T) {
var entry Entry var entry Entry
err := entry.Verify() err := entry.Verify()
if err == nil { if err == nil {
t.Error("an entry Without an ID must return an error") t.Error("an entry Without an ID must return an error")
} }
} }
func TestCreateEntryWithoutName(t *testing.T) { func TestCreateEntryWithoutName(t *testing.T) {
var entry Entry var entry Entry
entry.GenerateID() entry.GenerateID()
if entry.ID == "" { if entry.ID == "" {
t.Error("generateID can't be generate a void ID") t.Error("generateID can't be generate a void ID")
} }
err := entry.Verify() err := entry.Verify()
if err == nil { if err == nil {
t.Error("an entry without a name must return an error") t.Error("an entry without a name must return an error")
} }
} }
func TestCreateEntryWithName(t *testing.T) { func TestCreateEntryWithName(t *testing.T) {
entry := Entry{ Name: "test" } entry := Entry{Name: "test"}
entry.GenerateID() entry.GenerateID()
err := entry.Verify() err := entry.Verify()
if err != nil { if err != nil {
t.Errorf("an entry with a name mustn't return an error: %s", err) t.Errorf("an entry with a name mustn't return an error: %s", err)
} }
} }
func TestCreateEntryWithBadURI(t *testing.T) { func TestCreateEntryWithBadURI(t *testing.T) {
entry := Entry{ Name: "test", URI: "url/bad:" } entry := Entry{Name: "test", URI: "url/bad:"}
entry.GenerateID() entry.GenerateID()
err := entry.Verify() err := entry.Verify()
if err == nil { if err == nil {
t.Error("an entry with a bad URI must return an error") t.Error("an entry with a bad URI must return an error")
} }
} }
func TestCreateEntryWithGoodURI(t *testing.T) { func TestCreateEntryWithGoodURI(t *testing.T) {
entry := Entry{ Name: "test", URI: "http://localhost:8081" } entry := Entry{Name: "test", URI: "http://localhost:8081"}
entry.GenerateID() entry.GenerateID()
err := entry.Verify() err := entry.Verify()
if err != nil { if err != nil {
t.Errorf("an entry with a good URI mustn't return an error: %s", err) t.Errorf("an entry with a good URI mustn't return an error: %s", err)
} }
} }
func TestGenerateOTPCode(t *testing.T) { func TestGenerateOTPCode(t *testing.T) {
entry := Entry{ OTP: "JBSWY3DPEHPK3PXP" } entry := Entry{OTP: "JBSWY3DPEHPK3PXP"}
code, time, err := entry.OTPCode() code, time, err := entry.OTPCode()
if err != nil { if err != nil {
t.Errorf("must generate an OTP code without error: %s", err) t.Errorf("must generate an OTP code without error: %s", err)
} }
if len(code) != 6 { if len(code) != 6 {
t.Errorf("must generate an OTP code with 6 chars: %s", code) t.Errorf("must generate an OTP code with 6 chars: %s", code)
} }
if time < 0 || time > 30 { if time < 0 || time > 30 {
t.Errorf("time must be between 0 and 30: %d", time) t.Errorf("time must be between 0 and 30: %d", time)
} }
} }

View file

@ -14,58 +14,58 @@
package gpm package gpm
import( import (
"fmt" "flag"
"flag" "fmt"
"os" "os"
) )
// Options // Options
var( var (
ADD = flag.Bool("add", false, "add a new entry in the wallet") ADD = flag.Bool("add", false, "add a new entry in the wallet")
UPDATE = flag.Bool("update", false, "update an entry") UPDATE = flag.Bool("update", false, "update an entry")
DELETE = flag.Bool("delete", false, "delete an entry") DELETE = flag.Bool("delete", false, "delete an entry")
LIST = flag.Bool("list", false, "list the entries in a wallet") LIST = flag.Bool("list", false, "list the entries in a wallet")
LENGTH = flag.Int("length", 16, "specify the password length") LENGTH = flag.Int("length", 16, "specify the password length")
COPY = flag.Bool("copy", false, "enter an copy mode for an entry") COPY = flag.Bool("copy", false, "enter an copy mode for an entry")
CONFIG = flag.String("config", "", "specify the config file") CONFIG = flag.String("config", "", "specify the config file")
GROUP = flag.String("group", "", "search the entries in this group ") GROUP = flag.String("group", "", "search the entries in this group ")
WALLET = flag.String("wallet", "", "specify the wallet") WALLET = flag.String("wallet", "", "specify the wallet")
PATTERN = flag.String("pattern", "", "search the entries with this pattern") 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") 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") PASSWD = flag.Bool("password", false, "generate and print a random password")
DIGIT = flag.Bool("digit", false, "use digit to generate 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") 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") SPECIAL = flag.Bool("special", false, "use special chars to generate a random password")
EXPORT = flag.String("export", "", "json file path to export a wallet") EXPORT = flag.String("export", "", "json file path to export a wallet")
IMPORT = flag.String("import", "", "json file path to import entries") IMPORT = flag.String("import", "", "json file path to import entries")
HELP = flag.Bool("help", false, "print this help message") HELP = flag.Bool("help", false, "print this help message")
) )
// Run the cli interface // Run the cli interface
func Run() { func Run() {
var cli Cli var cli Cli
cli.Config.Load(*CONFIG) cli.Config.Load(*CONFIG)
flag.Parse() flag.Parse()
if *HELP { if *HELP {
flag.PrintDefaults() flag.PrintDefaults()
os.Exit(1) os.Exit(1)
} else if *PASSWD { } else if *PASSWD {
fmt.Println(RandomString(*LENGTH, *LETTER, *DIGIT, *SPECIAL)) fmt.Println(RandomString(*LENGTH, *LETTER, *DIGIT, *SPECIAL))
} else if *LIST { } else if *LIST {
cli.listEntry() cli.listEntry()
} else if *COPY { } else if *COPY {
cli.copyEntry() cli.copyEntry()
} else if *ADD { } else if *ADD {
cli.addEntry() cli.addEntry()
} else if *UPDATE { } else if *UPDATE {
cli.updateEntry() cli.updateEntry()
} else if *DELETE { } else if *DELETE {
cli.deleteEntry() cli.deleteEntry()
} else if *IMPORT != "" { } else if *IMPORT != "" {
cli.ImportWallet() cli.ImportWallet()
} else if *EXPORT != "" { } else if *EXPORT != "" {
cli.ExportWallet() cli.ExportWallet()
} }
} }

View file

@ -15,206 +15,205 @@
package gpm package gpm
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"regexp" "regexp"
"time" "sort"
"sort" "strings"
"strings" "time"
) )
// WalletFile contains the data in file // WalletFile contains the data in file
type WalletFile struct { type WalletFile struct {
Salt string Salt string
Data string Data string
} }
// Wallet struct have wallet informations // Wallet struct have wallet informations
type Wallet struct { type Wallet struct {
Name string Name string
Path string Path string
Salt string Salt string
Passphrase string Passphrase string
Entries []Entry Entries []Entry
} }
// Load all wallet's Entrys from the disk // Load all wallet's Entrys from the disk
func (w *Wallet) Load() error { func (w *Wallet) Load() error {
var walletFile WalletFile var walletFile WalletFile
_, err := os.Stat(w.Path) _, err := os.Stat(w.Path)
if err != nil { if err != nil {
return nil return nil
} }
content, err := ioutil.ReadFile(w.Path) content, err := ioutil.ReadFile(w.Path)
if err != nil { if err != nil {
return err return err
} }
err = json.Unmarshal(content, &walletFile) err = json.Unmarshal(content, &walletFile)
if err != nil { if err != nil {
return err return err
} }
w.Salt = walletFile.Salt w.Salt = walletFile.Salt
data, err := Decrypt(string(walletFile.Data), w.Passphrase, w.Salt) data, err := Decrypt(string(walletFile.Data), w.Passphrase, w.Salt)
if err != nil { if err != nil {
return err return err
} }
err = json.Unmarshal(data, &w.Entries) err = json.Unmarshal(data, &w.Entries)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
// Save the wallet on the disk // Save the wallet on the disk
func (w *Wallet) Save() error { func (w *Wallet) Save() error {
if w.Salt == "" { if w.Salt == "" {
w.Salt = RandomString(12, true, true, false) w.Salt = RandomString(12, true, true, false)
} }
data, err := json.Marshal(&w.Entries) data, err := json.Marshal(&w.Entries)
if err != nil { if err != nil {
return err return err
} }
dataEncrypted, err := Encrypt(data, w.Passphrase, w.Salt) dataEncrypted, err := Encrypt(data, w.Passphrase, w.Salt)
if err != nil { if err != nil {
return err return err
} }
walletFile := WalletFile{ Salt: w.Salt, Data: dataEncrypted } walletFile := WalletFile{Salt: w.Salt, Data: dataEncrypted}
content, err := json.Marshal(&walletFile) content, err := json.Marshal(&walletFile)
if err != nil { if err != nil {
return err return err
} }
err = ioutil.WriteFile(w.Path, content, 0600) err = ioutil.WriteFile(w.Path, content, 0600)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
// 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) []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 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))) ||
r.Match([]byte(strings.ToLower(entry.Comment))) || r.Match([]byte(strings.ToLower(entry.URI))) { r.Match([]byte(strings.ToLower(entry.Comment))) || r.Match([]byte(strings.ToLower(entry.URI))) {
entries = append(entries, entry) entries = append(entries, entry)
} }
} }
sort.Slice(entries, func(i, j int) bool { sort.Slice(entries, func(i, j int) bool {
return entries[i].Group < entries[j].Group return entries[i].Group < entries[j].Group
}) })
return entries return entries
} }
// SearchEntryByID return an Entry // SearchEntryByID return an Entry
func (w *Wallet) SearchEntryByID(id string) Entry { func (w *Wallet) SearchEntryByID(id string) Entry {
for _, entry := range w.Entries { for _, entry := range w.Entries {
if entry.ID == id { if entry.ID == id {
return entry return entry
} }
} }
return Entry{} return Entry{}
} }
// AddEntry append a new entry to wallet // AddEntry append a new entry to wallet
func (w *Wallet) AddEntry(entry Entry) error { func (w *Wallet) AddEntry(entry Entry) error {
err := entry.Verify() err := entry.Verify()
if err != nil { if err != nil {
return err return err
} }
if w.SearchEntryByID(entry.ID) != (Entry{}) { if w.SearchEntryByID(entry.ID) != (Entry{}) {
return fmt.Errorf("the id already exists in wallet, can't add the entry") return fmt.Errorf("the id already exists in wallet, can't add the entry")
} }
entry.Create = time.Now().Unix() entry.Create = time.Now().Unix()
entry.LastUpdate = entry.Create entry.LastUpdate = entry.Create
w.Entries = append(w.Entries, entry) w.Entries = append(w.Entries, entry)
return nil return nil
} }
// DeleteEntry delete an entry to wallet // DeleteEntry delete an entry to wallet
func (w *Wallet) DeleteEntry(id string) error { func (w *Wallet) DeleteEntry(id string) error {
for index, entry := range w.Entries { for index, entry := range w.Entries {
if entry.ID == id { if entry.ID == id {
w.Entries = append(w.Entries[:index], w.Entries[index+1:]...) w.Entries = append(w.Entries[:index], w.Entries[index+1:]...)
return nil return nil
} }
} }
return fmt.Errorf("entry not found with this id") return fmt.Errorf("entry not found with this id")
} }
// UpdateEntry update an Entry to wallet // UpdateEntry update an Entry to wallet
func (w *Wallet) UpdateEntry(entry Entry) error { func (w *Wallet) UpdateEntry(entry Entry) error {
oldEntry := w.SearchEntryByID(entry.ID) oldEntry := w.SearchEntryByID(entry.ID)
if oldEntry == (Entry{}) { if oldEntry == (Entry{}) {
return fmt.Errorf("entry not found with this id") return fmt.Errorf("entry not found with this id")
} }
err := entry.Verify() err := entry.Verify()
if err != nil { if err != nil {
return err return err
} }
entry.LastUpdate = time.Now().Unix() entry.LastUpdate = time.Now().Unix()
for index, i := range w.Entries { for index, i := range w.Entries {
if entry.ID == i.ID { if entry.ID == i.ID {
w.Entries[index] = entry w.Entries[index] = entry
return nil return nil
} }
} }
return fmt.Errorf("unknown error during the update") return fmt.Errorf("unknown error during the update")
} }
// Import a wallet from a json string // Import a wallet from a json string
func (w *Wallet) Import(data []byte) error { func (w *Wallet) Import(data []byte) error {
var entries []Entry var entries []Entry
err := json.Unmarshal([]byte(data), &entries) err := json.Unmarshal([]byte(data), &entries)
if err != nil { if err != nil {
return err return err
} }
for _, entry := range entries { for _, entry := range entries {
err = w.AddEntry(entry) err = w.AddEntry(entry)
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
} }
// Export a wallet to json // Export a wallet to json
func (w *Wallet) Export() ([]byte, error) { func (w *Wallet) Export() ([]byte, error) {
data, err := json.Marshal(&w.Entries) data, err := json.Marshal(&w.Entries)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
} }
return data, nil return data, nil
} }

View file

@ -1,235 +1,235 @@
package gpm package gpm
import ( import (
"fmt" "fmt"
"os" "io/ioutil"
"io/ioutil" "os"
"testing" "testing"
) )
func generateWalletWithEntries() Wallet { func generateWalletWithEntries() Wallet {
var wallet Wallet var wallet Wallet
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
entry := Entry{ ID: fmt.Sprintf("%d", i), Name: fmt.Sprintf("Entry %d", i), Group: "Good Group" } entry := Entry{ID: fmt.Sprintf("%d", i), Name: fmt.Sprintf("Entry %d", i), Group: "Good Group"}
wallet.AddEntry(entry) wallet.AddEntry(entry)
} }
return wallet return wallet
} }
func TestAddBadEntry(t *testing.T) { func TestAddBadEntry(t *testing.T) {
var entry Entry var entry Entry
var wallet Wallet var wallet Wallet
err := wallet.AddEntry(entry) err := wallet.AddEntry(entry)
if err == nil { if err == nil {
t.Error("a bad entry must return an error") t.Error("a bad entry must return an error")
} }
} }
func TestAddEntries(t *testing.T) { func TestAddEntries(t *testing.T) {
var wallet Wallet var wallet Wallet
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
entry := Entry{ ID: fmt.Sprintf("%d", i), Name: fmt.Sprintf("Entry %d", i) } entry := Entry{ID: fmt.Sprintf("%d", i), Name: fmt.Sprintf("Entry %d", i)}
err := wallet.AddEntry(entry) err := wallet.AddEntry(entry)
if err != nil { if err != nil {
t.Errorf("a good entry mustn't return an error: %s", err) t.Errorf("a good entry mustn't return an error: %s", err)
} }
} }
if len(wallet.Entries) != 10 { if len(wallet.Entries) != 10 {
t.Errorf("must have 10 entries: %d", len(wallet.Entries)) t.Errorf("must have 10 entries: %d", len(wallet.Entries))
} }
} }
func TestSearchEntryWithBadID(t *testing.T) { func TestSearchEntryWithBadID(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
entry := wallet.SearchEntryByID("BAD-ID") entry := wallet.SearchEntryByID("BAD-ID")
if entry.ID != "" { if entry.ID != "" {
t.Errorf("if the entry doesn't exist must return an empty Entry: %s", entry.ID) t.Errorf("if the entry doesn't exist must return an empty Entry: %s", entry.ID)
} }
} }
func TestSearchEntryWithGoodID(t *testing.T) { func TestSearchEntryWithGoodID(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
entry := wallet.SearchEntryByID("5") entry := wallet.SearchEntryByID("5")
if entry.ID != "5" { if entry.ID != "5" {
t.Errorf("the ID entry must be 5: %s", entry.ID) t.Errorf("the ID entry must be 5: %s", entry.ID)
} }
} }
func TestSearchEntriesByGroup(t *testing.T) { func TestSearchEntriesByGroup(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
entries := len(wallet.SearchEntry("", "BAD-GROUP")) 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")) 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)
} }
} }
func TestSearchEntriesByPattern(t *testing.T) { func TestSearchEntriesByPattern(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
entries := len(wallet.SearchEntry("BAD-PATTERN", "")) 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", "")) 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$", "")) 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)
} }
} }
func TestSearchEntriesByPatternAndGroup(t *testing.T) { func TestSearchEntriesByPatternAndGroup(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
entries := len(wallet.SearchEntry("entry", "good group")) 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)
} }
} }
func TestDeleteNotExistingEntry(t *testing.T) { func TestDeleteNotExistingEntry(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
err := wallet.DeleteEntry("BAD-ID") err := wallet.DeleteEntry("BAD-ID")
if err == nil { if err == nil {
t.Error("if the entry doesn't exist must return an error") t.Error("if the entry doesn't exist must return an error")
} }
if len(wallet.Entries) != 10 { if len(wallet.Entries) != 10 {
t.Errorf("must have 10 entries: %d", len(wallet.Entries)) t.Errorf("must have 10 entries: %d", len(wallet.Entries))
} }
} }
func TestDeleteEntry(t *testing.T) { func TestDeleteEntry(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
err := wallet.DeleteEntry("5") err := wallet.DeleteEntry("5")
if err != nil { if err != nil {
t.Errorf("a good entry mustn't return an error: %s", err) t.Errorf("a good entry mustn't return an error: %s", err)
} }
if len(wallet.Entries) != 9 { if len(wallet.Entries) != 9 {
t.Errorf("must have 9 entries: %d", len(wallet.Entries)) t.Errorf("must have 9 entries: %d", len(wallet.Entries))
} }
if wallet.SearchEntryByID("5").ID != "" { if wallet.SearchEntryByID("5").ID != "" {
t.Error("must return an empty entry for the ID 5") t.Error("must return an empty entry for the ID 5")
} }
} }
func TestUpdateNotExistingEntry(t *testing.T) { func TestUpdateNotExistingEntry(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
err := wallet.UpdateEntry(Entry{ID: "BAD-ID"}) err := wallet.UpdateEntry(Entry{ID: "BAD-ID"})
if err == nil { if err == nil {
t.Error("if the entry doesn't exist must return an error") t.Error("if the entry doesn't exist must return an error")
} }
} }
func TestUpdateEntry(t *testing.T) { func TestUpdateEntry(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
err := wallet.UpdateEntry(Entry{ID: "5"}) err := wallet.UpdateEntry(Entry{ID: "5"})
if err == nil { if err == nil {
t.Error("if the entry is bad must return an error") t.Error("if the entry is bad must return an error")
} }
err = wallet.UpdateEntry(Entry{ID: "5", Name: "Name 5"}) err = wallet.UpdateEntry(Entry{ID: "5", Name: "Name 5"})
if err != nil { if err != nil {
t.Errorf("a good entry mustn't return an error: %s", err) t.Errorf("a good entry mustn't return an error: %s", err)
} }
entry := wallet.SearchEntryByID("5") entry := wallet.SearchEntryByID("5")
if entry.Name != "Name 5" { if entry.Name != "Name 5" {
t.Errorf("the entry name for the ID 5 must be 'Name 5': %s", entry.Name) t.Errorf("the entry name for the ID 5 must be 'Name 5': %s", entry.Name)
} }
} }
func TestExportAndImport(t *testing.T) { func TestExportAndImport(t *testing.T) {
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
export, err := wallet.Export() export, err := wallet.Export()
if err != nil { if err != nil {
t.Errorf("an export mustn't return an error: %s", err) t.Errorf("an export mustn't return an error: %s", err)
} }
wallet = Wallet{} wallet = Wallet{}
err = wallet.Import(export) err = wallet.Import(export)
if err != nil { if err != nil {
t.Errorf("a good import mustn't return an error: %s", err) t.Errorf("a good import mustn't return an error: %s", err)
} }
entries := len(wallet.Entries) entries := len(wallet.Entries)
if entries != 10 { if entries != 10 {
t.Errorf("must have 10 entries: %d", entries) t.Errorf("must have 10 entries: %d", entries)
} }
} }
func TestSaveWallet(t *testing.T) { func TestSaveWallet(t *testing.T) {
tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-") tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-")
defer os.Remove(tmpFile.Name()) defer os.Remove(tmpFile.Name())
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
wallet.Path = tmpFile.Name() wallet.Path = tmpFile.Name()
wallet.Passphrase = "secret" wallet.Passphrase = "secret"
err := wallet.Save() err := wallet.Save()
if err != nil { if err != nil {
t.Errorf("save wallet mustn't return an error: %s", err) t.Errorf("save wallet mustn't return an error: %s", err)
} }
} }
func TestLoadWalletWithGoodPassword(t *testing.T) { func TestLoadWalletWithGoodPassword(t *testing.T) {
var loadWallet Wallet var loadWallet Wallet
tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-") tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-")
defer os.Remove(tmpFile.Name()) defer os.Remove(tmpFile.Name())
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
wallet.Path = tmpFile.Name() wallet.Path = tmpFile.Name()
wallet.Passphrase = "secret" wallet.Passphrase = "secret"
wallet.Save() wallet.Save()
loadWallet.Path = wallet.Path loadWallet.Path = wallet.Path
loadWallet.Passphrase = wallet.Passphrase loadWallet.Passphrase = wallet.Passphrase
err := loadWallet.Load() err := loadWallet.Load()
if err != nil { if err != nil {
t.Errorf("load wallet mustn't return an error: %s", err) t.Errorf("load wallet mustn't return an error: %s", err)
} }
entries := len(loadWallet.Entries) entries := len(loadWallet.Entries)
if entries != 10 { if entries != 10 {
t.Errorf("must have 10 entries: %d", entries) t.Errorf("must have 10 entries: %d", entries)
} }
} }
func TestLoadWalletWithBadPassword(t *testing.T) { func TestLoadWalletWithBadPassword(t *testing.T) {
var loadWallet Wallet var loadWallet Wallet
tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-") tmpFile, _ := ioutil.TempFile(os.TempDir(), "gpm_test-")
defer os.Remove(tmpFile.Name()) defer os.Remove(tmpFile.Name())
wallet := generateWalletWithEntries() wallet := generateWalletWithEntries()
wallet.Path = tmpFile.Name() wallet.Path = tmpFile.Name()
wallet.Passphrase = "secret" wallet.Passphrase = "secret"
wallet.Save() wallet.Save()
loadWallet.Path = wallet.Path loadWallet.Path = wallet.Path
loadWallet.Passphrase = "bad secret" loadWallet.Passphrase = "bad secret"
err := loadWallet.Load() err := loadWallet.Load()
if err == nil { if err == nil {
t.Error("load wallet with bad password must return an error") t.Error("load wallet with bad password must return an error")
} }
entries := len(loadWallet.Entries) entries := len(loadWallet.Entries)
if entries != 0 { if entries != 0 {
t.Errorf("must have 0 entries: %d", entries) t.Errorf("must have 0 entries: %d", entries)
} }
} }