From d47c22aff798c680c0b31e8b2de7cf5d495d5f5e Mon Sep 17 00:00:00 2001 From: Adrien Waksberg Date: Mon, 1 Jul 2019 08:03:41 +0200 Subject: [PATCH] first version --- .gitignore | 1 + CHANGELOG.md | 15 +++ LICENSE | 201 +++++++++++++++++++++++++++++++++++++ README.md | 72 ++++++++++++++ src/cli.go | 268 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.go | 87 ++++++++++++++++ src/crypto.go | 80 +++++++++++++++ src/entry.go | 56 +++++++++++ src/main.go | 49 +++++++++ src/wallet.go | 191 +++++++++++++++++++++++++++++++++++ 10 files changed, 1020 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/cli.go create mode 100644 src/config.go create mode 100644 src/crypto.go create mode 100644 src/entry.go create mode 100644 src/main.go create mode 100644 src/wallet.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36f971e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3d17348 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# CHANGELOG + +All notable changes to this project will be documented in this file. + +This project adheres to [Semantic Versioning](http://semver.org/). +Which is based on [Keep A Changelog](http://keepachangelog.com/) + +## Unreleased + +### Added + +- Save the wallet in AES-256 encrypted file +- Search entries with a pattern and/or by group +- Copy login, password and OTP code in clipboard +- Manage multiple wallets diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..afe21bd --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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 + + http://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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3f36a4 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# gpm: Go Passwords Manager + +[![Version](https://img.shields.io/badge/latest_version-1.0.0_dev-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) + +gpm is passwords manager write in go and use AES-256 to encrypt the wallets + +## Features + +- generate OTP code +- copy your login, password or otp in clipboard +- manage multiple wallets + +## Install + +### Build + +Download the sources and build + +```text +git clone https://git.yaegashi.fr/nishiki/gpm.git +cd gpm +go build -o bin/gpm src/*.go +``` + +Copy the binary in PATH: + +```text +sudo cp bin/gpm /usr/local/bin/gpm +``` + +## How to use + +```text +gpm -help + -add + add a new entry in the wallet + -config string + specify the config file + -copy + enter an copy mode for an entry + -delete + delete an entry + -group string + search the entries in this group + -help + print this help message + -list + list the entries in a wallet + -pattern string + search the entries with this pattern + -update + update an entry +``` + +## License + +```text +Copyright (c) 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 + + http://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. +``` diff --git a/src/cli.go b/src/cli.go new file mode 100644 index 0000000..2b7c4fa --- /dev/null +++ b/src/cli.go @@ -0,0 +1,268 @@ +// 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 main + +import( + "bufio" + "fmt" + "os" + "strconv" + "syscall" + "time" + "github.com/atotto/clipboard" + "github.com/olekukonko/tablewriter" + "github.com/pquerna/otp/totp" + "golang.org/x/crypto/ssh/terminal" +) + +// 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.Println(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() == "" { + fmt.Printf("\n") + 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] + } + + for true { + index, err := strconv.Atoi(c.input("Select the entry: ", "", true)) + if err == nil && index >= 0 && index + 1 <= len(entries) { + break + } + fmt.Println("your choice is not an integer or is out of range") + } + + 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, c.Config.WalletDefault), + 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) + entry.Password = c.input("Enter the 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)) + } + c.Wallet.Save() +} + +// 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) + 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() + + 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, _ := totp.GenerateCode(entry.OTP, time.Now()) + clipboard.WriteAll(code) + case "q": + os.Exit(0) + default: + fmt.Println("l -> copy login") + fmt.Println("p -> copy password") + fmt.Println("o -> copy OTP code") + fmt.Println("q -> quit") + } + } +} + +// Init the cli interface +func (c *Cli) Init() { + c.Config.Load(*CONFIG) +} + +// Run the cli interface +func (c *Cli) Run() { + if *LIST { + c.listEntry() + } else if *COPY { + c.copyEntry() + } else if *ADD { + c.addEntry() + } else if *UPDATE { + c.updateEntry() + } else if *DELETE { + c.deleteEntry() + } +} diff --git a/src/config.go b/src/config.go new file mode 100644 index 0000000..ea7f73f --- /dev/null +++ b/src/config.go @@ -0,0 +1,87 @@ +// 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 main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/user" + "runtime" +) + +// Config struct contain the config +type Config struct { + WalletDir string `json:"wallet_dir"` + WalletDefault string `json:"wallet_default"` +} + +// Init the configuration +func (c *Config) Init() error { + user, err := user.Current() + if err != nil { + return err + } + + if runtime.GOOS == "darwin" { + c.WalletDir = fmt.Sprintf("%s/Library/Preferences/mpw", user.HomeDir) + } else if runtime.GOOS == "windows" { + c.WalletDir = fmt.Sprintf("%s/AppData/Local/mpw", user.HomeDir) + } else { + c.WalletDir = fmt.Sprintf("%s/.config/mpw", user.HomeDir) + } + c.WalletDefault = "default" + + return nil +} + +// Load the configuration from a file +func (c *Config) Load(path string) error { + _, err := os.Stat(path) + if err != nil { + err = c.Init() + if err != nil { + return err + } + } + + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + err = json.Unmarshal(data, &c) + if err != nil { + return err + } + + return nil +} + +// Save the configuration +func (c *Config) Save(path string) error { + data, err := json.Marshal(&c) + if err != nil { + return err + } + + err = ioutil.WriteFile(path, []byte(data), 0644) + if err != nil { + return err + } + + return nil +} diff --git a/src/crypto.go b/src/crypto.go new file mode 100644 index 0000000..bd1d188 --- /dev/null +++ b/src/crypto.go @@ -0,0 +1,80 @@ +// 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 main + +import( + "crypto/aes" + "crypto/sha1" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "io" + + "golang.org/x/crypto/pbkdf2" +) + +// Encrypt data with aes256 +func Encrypt(data []byte, passphrase string, salt string) (string, error) { + key := pbkdf2.Key([]byte(passphrase), []byte(salt), 4096, 32, sha1.New) + + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", err + } + + cipher, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, cipher.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return "", err + } + + dataEncrypted := cipher.Seal(nonce, nonce, data, nil) + + return base64.StdEncoding.EncodeToString(dataEncrypted), nil +} + +// Decrypt data +func Decrypt(data string, passphrase string, salt string) ([]byte, error) { + key := pbkdf2.Key([]byte(passphrase), []byte(salt), 4096, 32, sha1.New) + + rawData, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return []byte{}, err + } + + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return []byte{}, err + } + + cipher, err := cipher.NewGCM(block) + if err != nil { + return []byte{}, err + } + + nonceSize := cipher.NonceSize() + nonce, text := rawData[:nonceSize], rawData[nonceSize:] + plaintext, err := cipher.Open(nil, nonce, text, nil) + if err != nil { + return []byte{}, err + } + + return plaintext, nil +} diff --git a/src/entry.go b/src/entry.go new file mode 100644 index 0000000..d696013 --- /dev/null +++ b/src/entry.go @@ -0,0 +1,56 @@ +// 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 main + +import( + "fmt" + "time" + "net/url" +) + +// Entry struct have the password informations +type Entry struct { + Name string `yaml:"name"` + ID string `yaml:"id"` + URI string `yaml:"uri"` + User string `yaml:"login"` + Password string `yaml:"password"` + OTP string `yaml:"otp"` + Group string `yaml:"group"` + Comment string `yaml:"comment"` +} + +// Verify if the item have'nt error +func (e *Entry) Verify() error { + if e.ID == "" { + return fmt.Errorf("you must generate an ID") + } + + if e.Name == "" { + return fmt.Errorf("you must define a name") + } + + uri, _ := url.Parse(e.URI) + if e.URI != "" && uri.Host == "" { + return fmt.Errorf("the uri isn't a valid uri") + } + + return nil +} + +// GenerateID create a new id for the entry +func (e *Entry) GenerateID() { + e.ID = fmt.Sprintf("%d", time.Now().UnixNano()) +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..70941f4 --- /dev/null +++ b/src/main.go @@ -0,0 +1,49 @@ +// 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 main + +import( + "flag" + "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") + 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 ") + PATTERN = flag.String("pattern", "", "search the entries with this pattern") + WALLET = flag.String("wallet", "", "specify the wallet") + HELP = flag.Bool("help", false, "print this help message") +) + +func init() { + flag.Parse() + + if *HELP { + flag.PrintDefaults() + os.Exit(1) + } +} + +func main() { + c := Cli{} + c.Init() + c.Run() +} diff --git a/src/wallet.go b/src/wallet.go new file mode 100644 index 0000000..59a7041 --- /dev/null +++ b/src/wallet.go @@ -0,0 +1,191 @@ +// 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 main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "os" + "regexp" + "sort" + "strings" +) + + +// WalletFile contains the data in file +type WalletFile struct { + Salt string + Data string +} + +// Wallet struct have wallet informations +type Wallet struct { + Name string + Path string + Salt string + Passphrase string + Entries []Entry +} + +// Load all wallet's Entrys from the disk +func (w *Wallet) Load() error { + var walletFile WalletFile + + _, err := os.Stat(w.Path) + if err != nil { + return nil + } + + content, err := ioutil.ReadFile(w.Path) + if err != nil { + return err + } + + err = json.Unmarshal(content, &walletFile) + if err != nil { + return err + } + + w.Salt = walletFile.Salt + data, err := Decrypt(string(walletFile.Data), w.Passphrase, w.Salt) + if err != nil { + return err + } + + err = json.Unmarshal(data, &w.Entries) + if err != nil { + return err + } + + return nil +} + +// Save the wallet on the disk +func (w *Wallet) Save() error { + if w.Salt == "" { + salt := make([]byte, 8) + for i := 0; i < 8; i++ { + salt[i] = byte(65 + rand.Intn(25)) + } + w.Salt = string(salt) + } + + data, err := json.Marshal(&w.Entries) + if err != nil { + return err + } + + dataEncrypted, err := Encrypt(data, w.Passphrase, w.Salt) + if err != nil { + return err + } + + walletFile := WalletFile{ Salt: w.Salt, Data: dataEncrypted } + content, err := json.Marshal(&walletFile) + if err != nil { + return err + } + + err = ioutil.WriteFile(w.Path, content, 0600) + if err != nil { + return err + } + + return nil +} + +// SearchEntry return an array with the array expected with the pattern +func (w *Wallet) SearchEntry(pattern string, group string) []Entry { + var entries []Entry + r := regexp.MustCompile(pattern) + + for _, entry := range w.Entries { + if group != "" && strings.ToLower(entry.Group) != strings.ToLower(group) { + continue + } + if r.Match([]byte(entry.Name)) || r.Match([]byte(entry.Comment)) || r.Match([]byte(entry.URI)) { + entries = append(entries, entry) + } + } + + sort.Slice(entries, func(i, j int) bool { + return entries[i].Group < entries[j].Group + }) + + return entries +} + +// SearchEntryByID return an Entry +func (w *Wallet) SearchEntryByID(id string) Entry { + for _, entry := range w.Entries { + if entry.ID == id { + return entry + } + } + + return Entry{} +} + +// AddEntry append a new entry to wallet +func (w *Wallet) AddEntry(entry Entry) error { + err := entry.Verify() + if err != nil { + return err + } + + if w.SearchEntryByID(entry.ID) != (Entry{}) { + return fmt.Errorf("the id already exists in wallet, can't add the entry") + } + + w.Entries = append(w.Entries, entry) + + return nil +} + +// DeleteEntry delete an entry to wallet +func (w *Wallet) DeleteEntry(id string) error { + for index, entry := range w.Entries { + if entry.ID == id { + w.Entries = append(w.Entries[:index], w.Entries[index+1:]...) + return nil + } + } + + return fmt.Errorf("entry not found with this id") +} + +// UpdateEntry update an Entry to wallet +func (w *Wallet) UpdateEntry(entry Entry) error { + oldEntry := w.SearchEntryByID(entry.ID) + if oldEntry == (Entry{}) { + return fmt.Errorf("entry not found with this id") + } + + err := entry.Verify() + if err != nil { + return err + } + + for index, i := range w.Entries { + if entry.ID == i.ID { + w.Entries[index] = entry + return nil + } + } + + return fmt.Errorf("unknown error during the update") +}