├── LICENSE
├── README.md
├── edit
└── edit.go
├── generate
├── generate.go
└── generate_test.go
├── go.mod
├── go.sum
├── initialize
└── initialize.go
├── insert
└── insert.go
├── passgo.go
├── pc
├── pc.go
└── pc_test.go
├── pio
└── pio.go
└── show
└── show.go
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Evan Johnson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # passgo
2 | stores, retrieves, generates, and synchronizes passwords and files securely and is written in Go! It is inspired by https://passwordstore.org but has a few key differences. The most important difference is passgo is not GPG based. Instead it uses a master password to securely store your passwords. It also supports encrypting arbitrary files.
3 |
4 |
5 |
6 | passgo is meant to be secure enough that you can publicly post your vault. I've started publishing my passwords [here](https://github.com/ejcx/passwords.git).
7 |
8 | ## Installation
9 |
10 | `passgo` requires Go version 1.11 or later.
11 |
12 | ```bash
13 | (cd; GO111MODULE=on go install github.com/ejcx/passgo/v2)
14 | ```
15 |
16 | ## Getting started with passgo
17 |
18 | Create a vault and specify the directory to store passwords in. You will be prompted for your master password:
19 |
20 | ```bash
21 | $ passgo init
22 | Please enter a strong master password:
23 | 2019/02/23 16:54:31 Created directory to store passwords: ~/.passgo
24 | ```
25 |
26 | Finally, to learn more you can either read about the commands listed in this README or run:
27 |
28 | ```bash
29 | passgo help
30 | ```
31 |
32 | The `--help` argument can be used on any subcommand to describe it and see documentation or examples.
33 |
34 | ## Configuring passgo
35 | The `PASSGODIR` environment variable specifies the directory that your vault is in.
36 |
37 | I store my vault in the default location `~/.passgo`. All subcommands will respect this environment variable, including `init`
38 |
39 |
40 | ## COMMANDS
41 |
42 | ### Listing Passwords
43 | ```
44 | $ passgo
45 | ├──money
46 | | └──mint.com
47 | └──another
48 | └──another.com
49 | ```
50 |
51 | This basic command is used to print out the contents of your password vault. It doesn't require you to enter your master password.
52 |
53 |
54 | ### Initializing Vault
55 | ```
56 | $ passgo init
57 | ```
58 | Init should only be run one time, before running any other command. It is used for generating your master public private keypair.
59 |
60 | By default, passgo will create your password vault in the `.passgo` directory within your home directory. You can override this location using the `PASSGODIR` environment variable.
61 |
62 |
63 |
64 | ### Inserting a password
65 | ```
66 | $ passgo insert money/mint.com
67 | Enter password for money/mint.com:
68 | ```
69 |
70 | Inserting a password in to your vault is easy. If you wish to group multiple entries together, it can be accomplished by prepending a group name followed by a slash to the pass-name.
71 |
72 | Here we are adding mint.com to the password store within the money group.
73 |
74 |
75 | ### Inserting a file
76 | ```
77 | $ passgo insert money/budget.csv budget.csv
78 | ```
79 |
80 | Adding a file works almost the same as insert. Instead it has an extra argument. The file that you want to add to your vault is the final argument.
81 |
82 |
83 | ### Retrieving a password
84 | ```
85 | $ passgo show money/mint.com
86 | Enter master password:
87 | dolladollabills$$1
88 | ```
89 |
90 | Show is used to display a password in standard out.
91 |
92 |
93 | ### Rename a password
94 | ```
95 | $ passgo rename mney/mint.com
96 | Enter new site name for mney/mint.com: money/mint.com
97 | ```
98 |
99 | If a password is added with the wrong name it can be updated later. Here we rename our mint.com site after misspelling the group name.
100 |
101 |
102 | ### Updating a password
103 | ```
104 | $ passgo edit money/mint.com
105 | Enter new password for money/mint.com:
106 | ```
107 |
108 | If you want to securely update a password for an already existing site, the edit command is helpful.
109 |
110 |
111 |
112 | ### Generating a password
113 | ```
114 | $ passgo generate
115 | %L4^!s,Rry!}s:U= %d, actual %d", tt.n, tt.expected, actual_length)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ejcx/passgo/v2
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/atotto/clipboard v0.1.1
7 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
8 | github.com/spf13/cobra v0.0.3
9 | github.com/spf13/pflag v1.0.3 // indirect
10 | golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f
11 | golang.org/x/sys v0.0.0-20190222171317-cd391775e71e // indirect
12 | )
13 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/atotto/clipboard v0.1.1 h1:WSoEbAS70E5gw8FbiqFlp69MGsB6dUb4l+0AGGLiVGw=
2 | github.com/atotto/clipboard v0.1.1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
4 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
5 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
6 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
7 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
8 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
9 | golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f h1:qWFY9ZxP3tfI37wYIs/MnIAqK0vlXp1xnYEa5HxFSSY=
10 | golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
11 | golang.org/x/sys v0.0.0-20190222171317-cd391775e71e h1:oF7qaQxUH6KzFdKN4ww7NpPdo53SZi4UlcksLrb2y/o=
12 | golang.org/x/sys v0.0.0-20190222171317-cd391775e71e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
13 |
--------------------------------------------------------------------------------
/initialize/initialize.go:
--------------------------------------------------------------------------------
1 | package initialize
2 |
3 | import (
4 | "crypto/rand"
5 | "fmt"
6 | "log"
7 | "os"
8 |
9 | "github.com/ejcx/passgo/v2/pc"
10 | "github.com/ejcx/passgo/v2/pio"
11 | "golang.org/x/crypto/nacl/box"
12 | )
13 |
14 | const (
15 | saltLen = 32
16 | configFound = "A passgo config file was already found."
17 | )
18 |
19 | // Init will initialize a new password vault in the home directory.
20 | func Init() {
21 | var needsDir bool
22 | var hasConfig bool
23 | var hasVault bool
24 |
25 | if dirExists, err := pio.PassDirExists(); err == nil {
26 | if !dirExists {
27 | needsDir = true
28 | } else {
29 | if _, err := pio.PassConfigExists(); err == nil {
30 | hasConfig = true
31 | }
32 | if _, err := pio.SitesVaultExists(); err == nil {
33 | hasVault = true
34 | }
35 | }
36 | }
37 |
38 | passDir, err := pio.GetPassDir()
39 | if err != nil {
40 | log.Fatalf("Could not get pass dir: %s", err.Error())
41 | }
42 | sitesFile, err := pio.GetSitesFile()
43 | if err != nil {
44 | log.Fatalf("Could not get sites dir: %s", err.Error())
45 | }
46 | configFile, err := pio.GetConfigPath()
47 | if err != nil {
48 | log.Fatalf("Could not get pass config: %s", err.Error())
49 | }
50 |
51 | // Prompt for the password immediately. The reason for doing this is
52 | // because if the user quits before the vault is fully initialized
53 | // (probably during password prompt since it's blocking), they will
54 | // be able to run init again a second time.
55 | pass, err := pio.PromptPass("Please enter a strong master password")
56 | if err != nil {
57 | log.Fatalf("Could not read password: %s", err.Error())
58 | }
59 |
60 | if needsDir {
61 | err = os.Mkdir(passDir, 0700)
62 | if err != nil {
63 | log.Fatalf("Could not create passgo vault: %s", err.Error())
64 | } else {
65 | fmt.Printf("Created directory to store passwords: %s\n", passDir)
66 | }
67 | }
68 | if fileDirExists, err := pio.PassFileDirExists(); err == nil {
69 | if !fileDirExists {
70 | encryptedFileDir, err := pio.GetEncryptedFilesDir()
71 | if err != nil {
72 | log.Fatalf("Could not get encrypted files dir: %s", err)
73 | }
74 | err = os.Mkdir(encryptedFileDir, 0700)
75 | if err != nil {
76 | log.Fatalf("Could not create encrypted file dir: %s", err)
77 | }
78 | }
79 | }
80 |
81 | // Don't just go around deleting things for users or prompting them
82 | // to delete things. Make them do this manaully. Maybe this saves 1
83 | // person an afternoon.
84 | if hasConfig {
85 | log.Fatalf(configFound)
86 | }
87 |
88 | // Create file with secure permission. os.Create() leaves file world-readable.
89 | config, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
90 | if err != nil {
91 | log.Fatalf("Could not create passgo config: %s", err.Error())
92 | }
93 | config.Close()
94 |
95 | // Handle creation and initialization of the site vault.
96 | if !hasVault {
97 | // Create file, with secure permissions.
98 | sf, err := os.OpenFile(sitesFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
99 | if err != nil {
100 | log.Fatalf("Could not create pass sites vault: %s", err.Error())
101 | }
102 | // Initialize an empty SiteFile
103 | siteFileContents := []byte("[]")
104 | _, err = sf.Write(siteFileContents)
105 | if err != nil {
106 | log.Fatalf("Could not save site file: %s", err.Error())
107 | }
108 | sf.Close()
109 | }
110 |
111 | // Generate a master password salt.
112 | var keySalt [32]byte
113 | _, err = rand.Read(keySalt[:])
114 | if err != nil {
115 | log.Fatalf("Could not generate random salt: %s", err.Error())
116 | }
117 |
118 | // Create a new salt for encrypting public key.
119 | var hmacSalt [32]byte
120 | _, err = rand.Read(hmacSalt[:])
121 | if err != nil {
122 | log.Fatalf("Could not generate random salt: %s", err.Error())
123 | }
124 |
125 | // kdf the master password.
126 | passKey, err := pc.Scrypt([]byte(pass), keySalt[:])
127 | if err != nil {
128 | log.Fatalf("Could not generate master key from pass: %s", err.Error())
129 | }
130 |
131 | pub, priv, err := box.GenerateKey(rand.Reader)
132 | if err != nil {
133 | log.Fatalf("Could not generate master key pair: %s", err.Error())
134 | }
135 |
136 | // Encrypt master private key with master password key.
137 | sealedMasterPrivKey, err := pc.Seal(&passKey, priv[:])
138 | if err != nil {
139 | log.Fatalf("Could not encrypt master key: %s", err.Error())
140 | }
141 |
142 | passConfig := pio.ConfigFile{
143 | MasterKeyPrivSealed: sealedMasterPrivKey,
144 | MasterPubKey: *pub,
145 | MasterPassKeySalt: keySalt,
146 | }
147 |
148 | if err = passConfig.SaveFile(); err != nil {
149 | log.Fatalf("Could not write to config file: %s", err.Error())
150 | }
151 | fmt.Println("Password Vault successfully initialized")
152 | }
153 |
--------------------------------------------------------------------------------
/insert/insert.go:
--------------------------------------------------------------------------------
1 | // Package insert handles adding a new site to the password store.
2 | package insert
3 |
4 | import (
5 | "crypto/rand"
6 | "encoding/json"
7 | "fmt"
8 | "io/ioutil"
9 | "log"
10 |
11 | "github.com/ejcx/passgo/v2/pc"
12 | "github.com/ejcx/passgo/v2/pio"
13 | "golang.org/x/crypto/nacl/box"
14 | )
15 |
16 | const (
17 | // PassPrompt is the string formatter that should be used
18 | // when prompting for a password.
19 | PassPrompt = "Enter password for %s"
20 | )
21 |
22 | // Password is used to add a new password entry to the vault.
23 | func Password(name string) {
24 | var c pio.ConfigFile
25 | pub, priv, err := box.GenerateKey(rand.Reader)
26 | if err != nil {
27 | log.Fatalf("Could not generate site key: %s", err.Error())
28 | }
29 |
30 | config, err := pio.GetConfigPath()
31 | if err != nil {
32 | log.Fatalf("Could not get config file name: %s", err.Error())
33 | }
34 |
35 | // Read the master public key.
36 | configContents, err := ioutil.ReadFile(config)
37 | if err != nil {
38 | log.Fatalf("Could not get config file contents: %s", err.Error())
39 | }
40 |
41 | err = json.Unmarshal(configContents, &c)
42 | if err != nil {
43 | log.Fatalf("Could not unmarshal config file contents: %s", err.Error())
44 | }
45 |
46 | masterPub := c.MasterPubKey
47 |
48 | sitePass, err := pio.PromptPass(fmt.Sprintf("Enter password for %s", name))
49 | if err != nil {
50 | log.Fatalf("Could not get password for site: %s", err.Error())
51 | }
52 |
53 | passSealed, err := pc.SealAsym([]byte(sitePass), &masterPub, priv)
54 | if err != nil {
55 | log.Fatalf("Could not seal new site password: %s", err.Error())
56 | }
57 |
58 | si := pio.SiteInfo{
59 | PubKey: *pub,
60 | Name: name,
61 | PassSealed: passSealed,
62 | }
63 |
64 | err = si.AddFile(passSealed, name)
65 | if err != nil {
66 | log.Fatalf("Could not save site file: %s", err.Error())
67 | }
68 | }
69 |
70 | // File is used to add a new file entry to the vault.
71 | func File(path, filename string) {
72 | var c pio.ConfigFile
73 | pub, priv, err := box.GenerateKey(rand.Reader)
74 | if err != nil {
75 | log.Fatalf("Could not generate site key: %s", err.Error())
76 | }
77 |
78 | config, err := pio.GetConfigPath()
79 | if err != nil {
80 | log.Fatalf("Could not get config file name: %s", err.Error())
81 | }
82 |
83 | // Read the master public key.
84 | configContents, err := ioutil.ReadFile(config)
85 | if err != nil {
86 | log.Fatalf("Could not get config file contents: %s", err.Error())
87 | }
88 |
89 | err = json.Unmarshal(configContents, &c)
90 | if err != nil {
91 | log.Fatalf("Could not unmarshal config file contents: %s", err.Error())
92 | }
93 |
94 | masterPub := c.MasterPubKey
95 |
96 | fileBytes, err := ioutil.ReadFile(filename)
97 | if err != nil {
98 | log.Fatalf("Could not open and read file that is being encrypted: %s", err.Error())
99 | }
100 |
101 | fileSealed, err := pc.SealAsym([]byte(fileBytes), &masterPub, priv)
102 | if err != nil {
103 | log.Fatalf("Could not seal file bytes: %s", err.Error())
104 | }
105 |
106 | si := pio.SiteInfo{
107 | PubKey: *pub,
108 | Name: path,
109 | IsFile: true,
110 | FileName: path,
111 | }
112 |
113 | err = si.AddFile(fileSealed, path)
114 | if err != nil {
115 | log.Fatalf("Could not save site file after file insert: %s", err.Error())
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/passgo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "runtime/debug"
6 | "strconv"
7 |
8 | "github.com/ejcx/passgo/v2/edit"
9 | "github.com/ejcx/passgo/v2/generate"
10 | "github.com/ejcx/passgo/v2/initialize"
11 | "github.com/ejcx/passgo/v2/insert"
12 | "github.com/ejcx/passgo/v2/pio"
13 | "github.com/ejcx/passgo/v2/show"
14 | "github.com/spf13/cobra"
15 | )
16 |
17 | var (
18 | copyPass bool
19 | RootCmd = &cobra.Command{
20 | Use: "passgo",
21 | Short: "Print the contents of the vault.",
22 | Long: `Print the contents of the vault. If you have
23 | not yet initialized your vault, it is necessary to run
24 | the init subcommand in order to create your passgo
25 | directory, and initialize your cryptographic keys.`,
26 | Run: func(cmd *cobra.Command, args []string) {
27 | if exists, _ := pio.PassFileDirExists(); exists {
28 | show.ListAll()
29 | } else {
30 | cmd.Help()
31 | }
32 | },
33 | }
34 | versionCmd = &cobra.Command{
35 | Use: "version",
36 | Short: "Print the version of your passgo binary.",
37 | Run: func(cmd *cobra.Command, args []string) {
38 | info, ok := debug.ReadBuildInfo()
39 | if !ok {
40 | fmt.Println("(unknown)")
41 | return
42 | }
43 | fmt.Println(info.Main.Version)
44 | },
45 | }
46 | initCmd = &cobra.Command{
47 | Use: "init",
48 | Short: "Initialize your passgo vault",
49 | Long: "Initialize the .passgo directory, and generate your secret keys",
50 | Run: func(cmd *cobra.Command, args []string) {
51 | initialize.Init()
52 | },
53 | }
54 | insertCmd = &cobra.Command{
55 | Use: "insert",
56 | Short: "Insert a file or password in to your vault",
57 | Example: "passgo insert money/bank.com",
58 | Args: cobra.RangeArgs(1, 2),
59 | Long: `Add a site to your password store. This site can optionally be a part
60 | of a group by prepending a group name and slash to the site name.
61 | Will prompt for confirmation when a site path is not unique.`,
62 | Run: func(cmd *cobra.Command, args []string) {
63 | if len(args) == 2 {
64 | path := args[0]
65 | filename := args[1]
66 | insert.File(path, filename)
67 | } else {
68 | pathName := args[0]
69 | insert.Password(pathName)
70 | }
71 | },
72 | }
73 | showCmd = &cobra.Command{
74 | Use: "show",
75 | Example: "passgo show money/bank.com",
76 | Short: "Print the password of a passgo entry.",
77 | Args: cobra.ExactArgs(1),
78 | Run: func(cmd *cobra.Command, args []string) {
79 | path := args[0]
80 | show.Site(path, copyPass)
81 | },
82 | }
83 | generateCmd = &cobra.Command{
84 | Use: "generate",
85 | Short: "Generate a secure password",
86 | Example: "passgo generate",
87 | Long: `Prints a randomly generated password. The length of this password defaults
88 | to 24. If a password length is specified as greater than 2048 then generate
89 | will fail.`,
90 | Args: cobra.RangeArgs(0, 1),
91 | Run: func(cmd *cobra.Command, args []string) {
92 | pwlen := -1
93 | if len(args) != 0 {
94 | pwlenStr := args[0]
95 | pwlenint, err := strconv.Atoi(pwlenStr)
96 | if err != nil {
97 | pwlen = -1
98 | } else {
99 | pwlen = pwlenint
100 | }
101 | }
102 | pass := generate.Generate(pwlen)
103 | fmt.Println(pass)
104 | },
105 | }
106 | findCmd = &cobra.Command{
107 | Use: "find",
108 | Aliases: []string{"ls"},
109 | Example: "passgo find bank.com",
110 | Short: "Find a site that contains the site-path.",
111 | Long: `Prints all sites that contain the site-path. Used to print just
112 | one group or all sites that contain a certain word in the group or name`,
113 | Args: cobra.ExactArgs(1),
114 | Run: func(cmd *cobra.Command, args []string) {
115 | path := args[0]
116 | show.Find(path)
117 | },
118 | }
119 | renameCmd = &cobra.Command{
120 | Use: "rename",
121 | Short: "Rename an entry in the password vault",
122 | Example: "passgo rename money/bank.com",
123 | Args: cobra.ExactArgs(1),
124 | Run: func(cmd *cobra.Command, args []string) {
125 | path := args[0]
126 | edit.Rename(path)
127 | },
128 | }
129 | editCmd = &cobra.Command{
130 | Use: "edit",
131 | Aliases: []string{"update"},
132 | Short: "Change the password of a site in the vault.",
133 | Example: "passgo edit money/bank.com",
134 | Args: cobra.ExactArgs(1),
135 | Run: func(cmd *cobra.Command, args []string) {
136 | path := args[0]
137 | edit.Edit(path)
138 | },
139 | }
140 | removeCmd = &cobra.Command{
141 | Use: "remove",
142 | Aliases: []string{"rm"},
143 | Example: "passgo remove money/bank.com",
144 | Short: "Remove a site from the password vault by specifying the entire site-path.",
145 | Args: cobra.ExactArgs(1),
146 | Run: func(cmd *cobra.Command, args []string) {
147 | path := args[0]
148 | edit.RemovePassword(path)
149 | },
150 | }
151 | )
152 |
153 | func init() {
154 | showCmd.PersistentFlags().BoolVarP(©Pass, "copy", "c", false, "Copy your password to the clipboard")
155 | RootCmd.AddCommand(findCmd)
156 | RootCmd.AddCommand(generateCmd)
157 | RootCmd.AddCommand(initCmd)
158 | RootCmd.AddCommand(insertCmd)
159 | RootCmd.AddCommand(removeCmd)
160 | RootCmd.AddCommand(editCmd)
161 | RootCmd.AddCommand(renameCmd)
162 | RootCmd.AddCommand(showCmd)
163 | RootCmd.AddCommand(versionCmd)
164 | }
165 |
166 | func main() {
167 | RootCmd.Execute()
168 | }
169 |
--------------------------------------------------------------------------------
/pc/pc.go:
--------------------------------------------------------------------------------
1 | // Package pc provides crypto functions for use by passgo. The purpose
2 | // of pc is to provide safe interfaces that factor confusing
3 | // and common programming errors away from programmers.
4 | package pc
5 |
6 | import (
7 | "crypto/rand"
8 | "encoding/hex"
9 | "encoding/json"
10 | "errors"
11 | "fmt"
12 | "io/ioutil"
13 | "log"
14 |
15 | "github.com/ejcx/passgo/v2/pio"
16 | "golang.org/x/crypto/curve25519"
17 | "golang.org/x/crypto/nacl/box"
18 | "golang.org/x/crypto/nacl/secretbox"
19 | "golang.org/x/crypto/scrypt"
20 | )
21 |
22 | const (
23 | MaxPwLength = 2048
24 | )
25 |
26 | var (
27 | // DigitLowerBound is the ascii digit lower bound.
28 | DigitLowerBound = 48
29 | // DigitUpperBound is the ascii digit upper bound.
30 | DigitUpperBound = 57
31 | // UpperCaseLowerBound is the ascii upper case lower bound.
32 | UpperCaseLowerBound = 65
33 | // UpperCaseUpperBound is the ascii upper case upper bound.
34 | UpperCaseUpperBound = 90
35 | // LowerCaseLowerBound is the ascii lower case lower bound.
36 | LowerCaseLowerBound = 97
37 | // LowerCaseUpperBound is the ascii lower case upper bound.
38 | LowerCaseUpperBound = 122
39 |
40 | // There are four groups of symbols in ascii table
41 | // It doesn't make sense to over engineer something
42 | // so just keep track of all the groups.
43 |
44 | // SymbolGrp1LowerBound is the ascii lowerbound of the first symbol grp.
45 | SymbolGrp1LowerBound = 33
46 | // SymbolGrp1UpperBound is the ascii lowerbound of the first symbol grp.
47 | SymbolGrp1UpperBound = 47
48 | // SymbolGrp2LowerBound is the ascii lowerbound of the first symbol grp.
49 | SymbolGrp2LowerBound = 58
50 | // SymbolGrp2UpperBound is the ascii lowerbound of the first symbol grp.
51 | SymbolGrp2UpperBound = 64
52 | // SymbolGrp3LowerBound is the ascii lowerbound of the first symbol grp.
53 | SymbolGrp3LowerBound = 91
54 | // SymbolGrp3UpperBound is the ascii lowerbound of the first symbol grp.
55 | SymbolGrp3UpperBound = 96
56 | // SymbolGrp4LowerBound is the ascii lowerbound of the first symbol grp.
57 | SymbolGrp4LowerBound = 123
58 | // SymbolGrp4UpperBound is the ascii lowerbound of the first symbol grp.
59 | SymbolGrp4UpperBound = 126
60 | )
61 |
62 | // PasswordSpecs indicates specifications for a desired generated password.
63 | type PasswordSpecs struct {
64 | NeedsUpper bool
65 | NeedsLower bool
66 | NeedsSymbol bool
67 | NeedsDigit bool
68 | }
69 |
70 | // Seal wraps that AEAD interface secretbox Seal and safely
71 | // generates a random nonce for developers. This change to
72 | // seal eliminates the risk of programmers reusing nonces.
73 | func Seal(key *[32]byte, message []byte) ([]byte, error) {
74 | var nonce [24]byte
75 | if _, err := rand.Read(nonce[:]); err != nil {
76 | return nil, err
77 | }
78 | return secretbox.Seal(nonce[:], message, &nonce, key), nil
79 | }
80 |
81 | // SealAsym wraps that AEAD interface box.Seal and safely generates
82 | // a random nonce for developers. This change to seal eliminates
83 | // the risk of programmers reusing nonces.
84 | func SealAsym(message []byte, pub *[32]byte, priv *[32]byte) (out []byte, err error) {
85 | var nonce [24]byte
86 | if _, err := rand.Read(nonce[:]); err != nil {
87 | return nil, err
88 | }
89 | sealedBytes := box.Seal(out, message, &nonce, pub, priv)
90 | return append(nonce[:], sealedBytes...), nil
91 | }
92 |
93 | // OpenAsym wraps the AEAD interface box.Open
94 | func OpenAsym(ciphertext []byte, pub, priv *[32]byte) (out []byte, err error) {
95 | var nonce [24]byte
96 | copy(nonce[:], ciphertext[:24])
97 | out, ok := box.Open(out[:0], ciphertext[24:], &nonce, pub, priv)
98 | if !ok {
99 | err = errors.New("Unable to decrypt message")
100 | }
101 | return
102 | }
103 |
104 | // Open wraps the AEAD interface secretbox.Open
105 | func Open(key *[32]byte, ciphertext []byte) (message []byte, err error) {
106 | var nonce [24]byte
107 | copy(nonce[:], ciphertext[:24])
108 | message, ok := secretbox.Open(message[:0], ciphertext[24:], &nonce, key)
109 | if !ok {
110 | err = errors.New("Unable to decrypt message")
111 | }
112 | return
113 | }
114 |
115 | // Scrypt is a wrapper around scrypt.Key that performs the Scrypt
116 | // algorithm on the input with opinionated defaults.
117 | func Scrypt(pass, salt []byte) (key [32]byte, err error) {
118 | keyBytes, err := scrypt.Key(pass, salt, 262144, 8, 1, 32)
119 | copy(key[:], keyBytes)
120 | return
121 | }
122 |
123 | // GetMasterKey is used to prompt user's for their password, read the
124 | // user's passgo config file and decrypt the master private key.
125 | func GetMasterKey() (masterPrivKey [32]byte) {
126 | pass, err := pio.PromptPass(pio.MasterPassPrompt)
127 | if err != nil {
128 | log.Fatalf("Could not get master password: %s", err.Error())
129 | }
130 | c, err := pio.GetConfigPath()
131 | if err != nil {
132 | log.Fatalf("Could not get config file: %s", err.Error())
133 | }
134 |
135 | var configFile pio.ConfigFile
136 | configFileBytes, err := ioutil.ReadFile(c)
137 | if err != nil {
138 | log.Fatalf("Could not read config file: %s", err.Error())
139 | }
140 | err = json.Unmarshal(configFileBytes, &configFile)
141 | if err != nil {
142 | log.Fatalf("Could not read unmarshal config file: %s", err.Error())
143 | }
144 | masterKey, err := Scrypt([]byte(pass), configFile.MasterPassKeySalt[:])
145 | if err != nil {
146 | log.Fatalf("Could not create master key: %s", err.Error())
147 | }
148 |
149 | masterPrivKeySlice, err := Open(&masterKey, configFile.MasterKeyPrivSealed)
150 |
151 | copy(masterPrivKey[:], masterPrivKeySlice)
152 | if err != nil {
153 | log.Fatalf("Could not decrypt private key: %s", err.Error())
154 | }
155 |
156 | // Sanity check the public key that is stored in the config file.
157 | // If the public key has changed then we should error out and
158 | // let the user know.
159 | publicKey := new([32]byte)
160 | curve25519.ScalarBaseMult(publicKey, &masterPrivKey)
161 | if *publicKey != configFile.MasterPubKey {
162 | log.Fatalf("Vault integrity cannot be verified: %s", errors.New("Wrong master public key"))
163 | }
164 |
165 | return
166 | }
167 |
168 | func checkBound(letter byte, lowerBound, upperBound int) bool {
169 | if int(letter) >= lowerBound && int(letter) <= upperBound {
170 | return true
171 | }
172 | return false
173 | }
174 | func isASCIIDigit(letter byte) bool {
175 | return checkBound(letter, DigitLowerBound, DigitUpperBound)
176 | }
177 | func isASCIIUpper(letter byte) bool {
178 | return checkBound(letter, UpperCaseLowerBound, UpperCaseUpperBound)
179 | }
180 | func isASCIILower(letter byte) bool {
181 | return checkBound(letter, LowerCaseLowerBound, LowerCaseUpperBound)
182 | }
183 | func isASCIISymbol(letter byte) bool {
184 | grp1 := checkBound(letter, SymbolGrp1LowerBound, SymbolGrp1UpperBound)
185 | grp2 := checkBound(letter, SymbolGrp2LowerBound, SymbolGrp2UpperBound)
186 | grp3 := checkBound(letter, SymbolGrp3LowerBound, SymbolGrp3UpperBound)
187 | grp4 := checkBound(letter, SymbolGrp4LowerBound, SymbolGrp4UpperBound)
188 | return grp1 || grp2 || grp3 || grp4
189 | }
190 |
191 | func passwordExpectationsPossible(specs *PasswordSpecs, passlen int) bool {
192 | minLength := 0
193 | if specs.NeedsUpper {
194 | minLength++
195 | }
196 | if specs.NeedsLower {
197 | minLength++
198 | }
199 | if specs.NeedsSymbol {
200 | minLength++
201 | }
202 | if specs.NeedsDigit {
203 | minLength++
204 | }
205 | if passlen < minLength {
206 | return false
207 | }
208 | return true
209 | }
210 |
211 | // GeneratePassword is used to generate a password like string securely.
212 | // GeneratePassword has no upper limit to the length of a password that
213 | // it can generate, but is restricted by the size of int.
214 | // It requires generation of a string password that has a upper case
215 | // letter, a lower case letter, a symbol, and a number.
216 | //
217 | // It works by reading a big block of randomness from the crypto rand
218 | // package and searching for printable characters. It will continue
219 | // to read chunks of randomness until it has found a password that
220 | // meets the specifications of the PasswordSpec passed in to the func.
221 | func GeneratePassword(specs *PasswordSpecs, passlen int) (pass string, err error) {
222 | var (
223 | letters [65535]byte
224 | )
225 | if !passwordExpectationsPossible(specs, passlen) {
226 | err = errors.New("Invalid password specs and length passed in to generate password. Try generating a longer password")
227 | return
228 | }
229 | if passlen > MaxPwLength {
230 | err = fmt.Errorf("Max password length is %d. Generate a shorter password", MaxPwLength)
231 | return
232 | }
233 | for {
234 | pass = ""
235 | _, err = rand.Read(letters[:])
236 | if err != nil {
237 | return
238 | }
239 |
240 | for _, letter := range letters {
241 | // Check to make sure that the letter is inside
242 | // the range of printable characters
243 | if letter > 32 && letter < 127 {
244 | pass += string(letter)
245 | }
246 | // If it doesn't meet the specs, but we verified earlier that it is
247 | // possible to meet the pw expectations, just try again.
248 | if passlen == len(pass) {
249 | if specs.MeetsSpecs(pass) {
250 | return
251 | }
252 | continue
253 | }
254 | }
255 | }
256 | }
257 |
258 | func (specs *PasswordSpecs) MeetsSpecs(pass string) bool {
259 | var (
260 | needsUpper = specs.NeedsUpper
261 | needsLower = specs.NeedsLower
262 | needsSymbol = specs.NeedsSymbol
263 | needsDigit = specs.NeedsDigit
264 | )
265 | for i := 0; i < len(pass); i++ {
266 | if isASCIIDigit(pass[i]) {
267 | needsDigit = false
268 | } else if isASCIIUpper(pass[i]) {
269 | needsUpper = false
270 | } else if isASCIILower(pass[i]) {
271 | needsLower = false
272 | } else if isASCIISymbol(pass[i]) {
273 | needsSymbol = false
274 | }
275 | // Optimization. Once we find out that we have everything
276 | // that we need, return.
277 | if !needsUpper && !needsLower && !needsSymbol && !needsDigit {
278 | return !needsUpper && !needsLower && !needsSymbol && !needsDigit
279 | }
280 | }
281 | // The answer is false if the optmiziation didn't return true.
282 | return false
283 | }
284 |
285 | // GenHexString will generate a random 32 character hex string.
286 | func GenHexString() (string, error) {
287 | var b [16]byte
288 | _, err := rand.Read(b[:])
289 | if err != nil {
290 | return "", err
291 | }
292 | return hex.EncodeToString(b[:]), nil
293 | }
294 |
--------------------------------------------------------------------------------
/pc/pc_test.go:
--------------------------------------------------------------------------------
1 | package pc
2 |
3 | import (
4 | "encoding/hex"
5 | "testing"
6 | )
7 |
8 | func TestGenHexString(t *testing.T) {
9 | g, err := GenHexString()
10 | if err != nil {
11 | t.Errorf("Could not generate hex string: %s", err)
12 | }
13 | _, err = hex.DecodeString(g)
14 | if err != nil {
15 | t.Errorf("Could not decode hex string: %s", err)
16 | }
17 | }
18 |
19 | func TestGeneratePassword(t *testing.T) {
20 | pass, err := GeneratePassword(&PasswordSpecs{}, 20)
21 | if err != nil {
22 | t.Fatalf("Could not generate password: %s", err)
23 | }
24 | if len(pass) == 0 {
25 | t.Fatalf("Bad length of password. Should never be 0")
26 | }
27 | }
28 |
29 | func TestGenerateImpossiblePassword(t *testing.T) {
30 | ps := &PasswordSpecs{
31 | NeedsUpper: true,
32 | NeedsLower: true,
33 | NeedsSymbol: true,
34 | NeedsDigit: true,
35 | }
36 | _, err := GeneratePassword(ps, 3)
37 | if err == nil {
38 | t.Fatalf("Impossible password request did not throw an error")
39 | }
40 | }
41 |
42 | func TestGenerateShortPassword(t *testing.T) {
43 | ps := &PasswordSpecs{
44 | NeedsUpper: true,
45 | NeedsLower: true,
46 | NeedsSymbol: true,
47 | NeedsDigit: true,
48 | }
49 | pass, err := GeneratePassword(ps, 4)
50 | if err != nil {
51 | t.Fatalf("Could not generate password: %s", err)
52 | }
53 | if len(pass) != 4 {
54 | t.Fatalf("Bad length of password. Should be 4")
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/pio/pio.go:
--------------------------------------------------------------------------------
1 | package pio
2 |
3 | import (
4 | "bufio"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io/ioutil"
9 | "log"
10 | "os"
11 | "os/signal"
12 | "os/user"
13 | "path/filepath"
14 |
15 | "github.com/atotto/clipboard"
16 |
17 | "golang.org/x/crypto/ssh/terminal"
18 | )
19 |
20 | const (
21 | PASSGODIR = "PASSGODIR"
22 | // ConfigFileName is the name of the passgo config file.
23 | ConfigFileName = "config"
24 | // SiteFileName is the name of the passgo password store file.
25 | SiteFileName = "sites.json"
26 | // EncryptedFileDir is the name of the passgo encrypted file dir.
27 | EncryptedFileDir = "files"
28 | )
29 |
30 | var (
31 | // MasterPassPrompt is the standard prompt string for all passgo
32 | MasterPassPrompt = "Enter master password"
33 | )
34 |
35 | // PassFile is an interface for how all passgo files should be saved.
36 | type PassFile interface {
37 | SaveFile() (err error)
38 | }
39 |
40 | // ConfigFile represents the passgo config file.
41 | type ConfigFile struct {
42 | MasterKeyPrivSealed []byte
43 | PubKeyHmac []byte
44 | SiteHmac []byte
45 | MasterPubKey [32]byte
46 | MasterPassKeySalt [32]byte
47 | HmacSalt [32]byte
48 | SiteHmacSalt [32]byte
49 | }
50 |
51 | // SiteInfo represents a single saved password entry.
52 | type SiteInfo struct {
53 | PubKey [32]byte
54 | PassSealed []byte
55 | Name string
56 | FileName string
57 | IsFile bool
58 | }
59 |
60 | // SiteFile represents the entire passgo password store.
61 | type SiteFile []SiteInfo
62 |
63 | func PassFileDirExists() (bool, error) {
64 | d, err := GetEncryptedFilesDir()
65 | if err != nil {
66 | return false, err
67 | }
68 | dirInfo, err := os.Stat(d)
69 | if err == nil {
70 | if dirInfo.IsDir() {
71 | return true, nil
72 | }
73 | } else {
74 | if os.IsNotExist(err) {
75 | return false, nil
76 | }
77 | }
78 | return false, err
79 | }
80 |
81 | // PassDirExists is used to determine if the passgo
82 | // directory in the user's home directory exists.
83 | func PassDirExists() (bool, error) {
84 | d, err := GetPassDir()
85 | if err != nil {
86 | return false, err
87 | }
88 | dirInfo, err := os.Stat(d)
89 | if err == nil {
90 | if !dirInfo.IsDir() {
91 | return true, nil
92 | }
93 | } else {
94 | if os.IsNotExist(err) {
95 | return false, nil
96 | }
97 | }
98 | return false, err
99 | }
100 |
101 | // PassConfigExists is used to determine if the passgo config
102 | // file exists in the user's passgo directory.
103 | func PassConfigExists() (bool, error) {
104 | c, err := GetConfigPath()
105 | if err != nil {
106 | return false, err
107 | }
108 | _, err = os.Stat(c)
109 | if err != nil {
110 | return false, err
111 | }
112 | return true, nil
113 | }
114 |
115 | // SitesVaultExists is used to determine if the password store
116 | // exists in the user's passgo directory.
117 | func SitesVaultExists() (bool, error) {
118 | c, err := GetConfigPath()
119 | if err != nil {
120 | return false, err
121 | }
122 | sitesFilePath := filepath.Join(c, SiteFileName)
123 | _, err = os.Stat(sitesFilePath)
124 | if err != nil {
125 | return false, err
126 | }
127 | return true, nil
128 | }
129 |
130 | func GetHomeDir() (d string, err error) {
131 | usr, err := user.Current()
132 | if err == nil {
133 | d = usr.HomeDir
134 | }
135 | return
136 | }
137 |
138 | // GetPassDir is used to return the user's passgo directory.
139 | func GetPassDir() (d string, err error) {
140 | d, ok := os.LookupEnv(PASSGODIR)
141 | if !ok {
142 | home, err := GetHomeDir()
143 | if err == nil {
144 | d = filepath.Join(home, ".passgo")
145 | }
146 | }
147 | return
148 | }
149 |
150 | // GetConfigPath is used to get the user's passgo directory.
151 | func GetConfigPath() (p string, err error) {
152 | d, err := GetPassDir()
153 | if err == nil {
154 | p = filepath.Join(d, ConfigFileName)
155 | }
156 | return
157 | }
158 |
159 | // GetEncryptedFilesDir is used to get the directory that we store
160 | // encrypted files in.
161 | func GetEncryptedFilesDir() (p string, err error) {
162 | d, err := GetPassDir()
163 | if err == nil {
164 | p = filepath.Join(d, EncryptedFileDir)
165 | }
166 | return
167 | }
168 |
169 | // GetSitesFile will return the user's passgo vault.
170 | func GetSitesFile() (d string, err error) {
171 | p, err := GetPassDir()
172 | if err == nil {
173 | d = filepath.Join(p, SiteFileName)
174 | }
175 | return
176 | }
177 |
178 | func (s *SiteInfo) AddFile(fileBytes []byte, filename string) error {
179 | encFileDir, err := GetEncryptedFilesDir()
180 | if err != nil {
181 | return err
182 | }
183 | // Make sure that the file directory exists.
184 | fileDirExists, err := PassFileDirExists()
185 | if err != nil {
186 | return err
187 | }
188 | if !fileDirExists {
189 | err = os.Mkdir(encFileDir, 0700)
190 | if err != nil {
191 | log.Fatalf("Could not create passgo encrypted file dir: %s", err.Error())
192 | }
193 | }
194 | encFilePath := filepath.Join(encFileDir, filename)
195 | dir, _ := filepath.Split(encFilePath)
196 | err = os.MkdirAll(dir, 0700)
197 | if err != nil {
198 | log.Fatalf("Could not create subdirectory: %s", err.Error())
199 | }
200 | err = ioutil.WriteFile(encFilePath, fileBytes, 0666)
201 | if err != nil {
202 | return err
203 | }
204 |
205 | // We still need to add this site info to the bytes.
206 | return s.AddSite()
207 | }
208 |
209 | // AddSite is used by individual password entries to update the vault.
210 | func (s *SiteInfo) AddSite() (err error) {
211 | siteFile := GetVault()
212 | for _, si := range siteFile {
213 | if s.Name == si.Name {
214 | return errors.New("Could not add site with duplicate name")
215 | }
216 | }
217 | siteFile = append(siteFile, *s)
218 | return UpdateVault(siteFile)
219 | }
220 |
221 | // GetVault is used to retrieve the password vault for the user.
222 | func GetVault() (s SiteFile) {
223 | si, err := GetSitesFile()
224 | if err != nil {
225 | log.Fatalf("Could not get pass dir: %s", err.Error())
226 | }
227 | siteFileContents, err := ioutil.ReadFile(si)
228 | if err != nil {
229 | if os.IsNotExist(err) {
230 | log.Fatalf("Could not open site file. Run passgo init.: %s", err.Error())
231 | }
232 | log.Fatalf("Could not read site file: %s", err.Error())
233 | }
234 | err = json.Unmarshal(siteFileContents, &s)
235 | if err != nil {
236 | log.Fatalf("Could not unmarshal site info: %s", err.Error())
237 | }
238 | return
239 | }
240 |
241 | // GetSiteFileBytes returns the bytes instead of a SiteFile
242 | func GetSiteFileBytes() (b []byte) {
243 | si, err := GetSitesFile()
244 | if err != nil {
245 | log.Fatalf("Could not get pass dir: %s", err.Error())
246 | }
247 | f, err := os.OpenFile(si, os.O_RDWR, 0600)
248 | if err != nil {
249 | log.Fatalf("Could not open site file: %s", err.Error())
250 | }
251 | defer f.Close()
252 | b, err = ioutil.ReadAll(f)
253 | if err != nil {
254 | log.Fatalf("Could not read site file: %s", err.Error())
255 | }
256 | return
257 | }
258 |
259 | // UpdateVault is used to replace the current password vault.
260 | func UpdateVault(s SiteFile) (err error) {
261 | si, err := GetSitesFile()
262 | if err != nil {
263 | log.Fatalf("Could not get pass dir: %s", err.Error())
264 | }
265 | siteFileContents, err := json.MarshalIndent(s, "", "\t")
266 | if err != nil {
267 | log.Fatalf("Could not marshal site info: %s", err.Error())
268 | }
269 |
270 | // Write the site with the newly appended site to the file.
271 | err = ioutil.WriteFile(si, siteFileContents, 0666)
272 | return
273 | }
274 |
275 | // SaveFile is used by ConfigFiles to update the passgo config.
276 | func (c *ConfigFile) SaveFile() (err error) {
277 | if exists, err := PassConfigExists(); err != nil {
278 | log.Fatalf("Could not find config file: %s", err.Error())
279 | } else {
280 | if !exists {
281 | log.Fatalf("pass config could not be found: %s", err.Error())
282 | }
283 | }
284 | cBytes, err := json.MarshalIndent(c, "", "\t")
285 | if err != nil {
286 | log.Fatalf("Could not marshal config file: %s", err.Error())
287 | }
288 | path, err := GetConfigPath()
289 | if err != nil {
290 | log.Fatalf("Could not get config file path: %s", err.Error())
291 | }
292 | err = ioutil.WriteFile(path, cBytes, 0666)
293 | return
294 | }
295 |
296 | // ReadConfig is used to return the passgo ConfigFile.
297 | func ReadConfig() (c ConfigFile, err error) {
298 | config, err := GetConfigPath()
299 | if err != nil {
300 | return
301 | }
302 | configBytes, err := ioutil.ReadFile(config)
303 | if err != nil {
304 | return
305 | }
306 | err = json.Unmarshal(configBytes, &c)
307 | return
308 | }
309 |
310 | // PromptPass will prompt user's for a password by terminal.
311 | func PromptPass(prompt string) (pass string, err error) {
312 | // Make a copy of STDIN's state to restore afterward
313 | fd := int(os.Stdin.Fd())
314 | oldState, err := terminal.GetState(fd)
315 | if err != nil {
316 | panic("Could not get state of terminal: " + err.Error())
317 | }
318 | defer terminal.Restore(fd, oldState)
319 |
320 | // Restore STDIN in the event of a signal interuption
321 | sigch := make(chan os.Signal, 1)
322 | signal.Notify(sigch, os.Interrupt)
323 | go func() {
324 | for _ = range sigch {
325 | terminal.Restore(fd, oldState)
326 | os.Exit(1)
327 | }
328 | }()
329 |
330 | fmt.Printf("%s: ", prompt)
331 | passBytes, err := terminal.ReadPassword(fd)
332 | fmt.Println("")
333 | return string(passBytes), err
334 | }
335 |
336 | // Prompt will prompt a user for regular data from stdin.
337 | func Prompt(prompt string) (s string, err error) {
338 | fmt.Printf("%s", prompt)
339 | stdin := bufio.NewReader(os.Stdin)
340 | l, _, err := stdin.ReadLine()
341 | return string(l), err
342 | }
343 |
344 | func ToClipboard(s string) {
345 | if err := clipboard.WriteAll(s); err != nil {
346 | log.Fatalf("Could not copy password to clipboard: %s", err.Error())
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/show/show.go:
--------------------------------------------------------------------------------
1 | package show
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "os"
9 | "path/filepath"
10 | "runtime"
11 | "strings"
12 |
13 | "github.com/ejcx/passgo/v2/pc"
14 | "github.com/ejcx/passgo/v2/pio"
15 | )
16 |
17 | type searchType int
18 |
19 | var (
20 | lastPrefix = "└──"
21 | regPrefix = "├──"
22 | innerPrefix = "| "
23 | innerLastPrefix = " "
24 | )
25 |
26 | const (
27 | // All indicates SearchSites should return all sites from the vault.
28 | All searchType = iota
29 | // One indicates SearchSites should return only one site from the vault.
30 | // It is used when printing a site.
31 | One
32 | // Search indicates that SearchSites should return all sites found that
33 | // match that contain the searchFor string
34 | Search
35 | )
36 |
37 | func init() {
38 | /* Windows doesn't work with ambiguous width characters */
39 | if runtime.GOOS == "windows" {
40 | lastPrefix = "+--"
41 | regPrefix = "+--"
42 | }
43 | }
44 |
45 | func handleErrors(allErrors []error) {
46 | errorStr := "Error"
47 | if len(allErrors) == 0 {
48 | return
49 | } else if len(allErrors) > 1 {
50 | errorStr = "Errors"
51 | }
52 | log.Printf("%d %s encountered:\n", len(allErrors), errorStr)
53 | for n, err := range allErrors {
54 | log.Printf("Error %d: %s", n, err.Error())
55 | }
56 | }
57 |
58 | // Find will search the vault for all occurences of frag in the site name.
59 | func Find(frag string) {
60 | allSites, allErrors := SearchAll(Search, frag)
61 | showResults(allSites)
62 | handleErrors(allErrors)
63 | }
64 |
65 | // Site will print out the password of the site that matches path
66 | func Site(path string, copyPassword bool) {
67 | allSites, allErrors := SearchAll(One, path)
68 | if len(allSites) == 0 {
69 | fmt.Printf("Site with path %s not found", path)
70 | return
71 | }
72 | masterPrivKey := pc.GetMasterKey()
73 | showPassword(allSites, masterPrivKey, copyPassword)
74 | handleErrors(allErrors)
75 | }
76 |
77 | // ListAll will print out all contents of the vault.
78 | func ListAll() {
79 | allSites, allErrors := SearchAll(All, "")
80 | showResults(allSites)
81 | handleErrors(allErrors)
82 | }
83 |
84 | func showPassword(allSites map[string][]pio.SiteInfo, masterPrivKey [32]byte, copyPassword bool) {
85 | for _, siteList := range allSites {
86 | for _, site := range siteList {
87 | var unsealed []byte
88 | var err error
89 | if site.IsFile {
90 | fileDir, err := pio.GetEncryptedFilesDir()
91 | if err != nil {
92 | log.Fatalf("Could not get encrypted file dir when searching vault: %s", err.Error())
93 | }
94 | filePath := filepath.Join(fileDir, site.FileName)
95 | f, err := os.OpenFile(filePath, os.O_RDONLY, 0600)
96 | if err != nil {
97 | log.Fatalf("Could not open encrypted file: %s", err.Error())
98 | }
99 | defer f.Close()
100 |
101 | fileSealed, err := ioutil.ReadAll(f)
102 | if err != nil {
103 | log.Fatalf("Could not read encrypted file: %s", err.Error())
104 | }
105 | unsealed, err = pc.OpenAsym(fileSealed, &site.PubKey, &masterPrivKey)
106 | if err != nil {
107 | log.Fatalf("Could not decrypt file bytes: %s", err.Error())
108 | }
109 |
110 | } else {
111 | unsealed, err = pc.OpenAsym(site.PassSealed, &site.PubKey, &masterPrivKey)
112 | if err != nil {
113 | log.Println("Could not decrypt site password.")
114 | continue
115 | }
116 | }
117 | if copyPassword {
118 | pio.ToClipboard(string(unsealed))
119 | } else {
120 | fmt.Println(string(unsealed))
121 | }
122 | }
123 | }
124 | }
125 |
126 | func showResults(allSites map[string][]pio.SiteInfo) {
127 | fmt.Println(".")
128 | counter := 1
129 | for group, siteList := range allSites {
130 | siteCounter := 1
131 | for _, site := range siteList {
132 | preGroup := regPrefix
133 | preName := innerPrefix + regPrefix
134 | if counter == len(allSites) {
135 | preGroup = lastPrefix
136 | sitePrefix := innerLastPrefix
137 | if group == "" {
138 | sitePrefix = ""
139 | }
140 | preName = sitePrefix + regPrefix
141 | if siteCounter == len(siteList) {
142 | preName = sitePrefix + lastPrefix
143 | }
144 | } else {
145 | if siteCounter == len(siteList) {
146 | preName = innerPrefix + lastPrefix
147 | }
148 | }
149 |
150 | if siteCounter == 1 {
151 | if group != "" {
152 | fmt.Println(preGroup + group)
153 | }
154 | }
155 | fmt.Printf("%s%s\n", preName, site.Name)
156 | siteCounter++
157 | }
158 | counter++
159 | }
160 | }
161 |
162 | // SearchAll will perform a search of searchType with optionally used searchFor. It
163 | // will return all sites as a map of group names to pio.SiteInfo types. That way, callers
164 | // of this function do not need to sort the sites by group themselves.
165 | func SearchAll(st searchType, searchFor string) (allSites map[string][]pio.SiteInfo, allErrors []error) {
166 | allSites = map[string][]pio.SiteInfo{}
167 | siteFile, err := pio.GetSitesFile()
168 | if err != nil {
169 | log.Fatalf("Could not get site file: %s", err.Error())
170 | }
171 |
172 | siteFileContents, err := ioutil.ReadFile(siteFile)
173 | if err != nil {
174 | if os.IsNotExist(err) {
175 | log.Fatalf("Could not open site file. Run passgo init.: %s", err.Error())
176 | }
177 | log.Fatalf("Could not read site file contents: %s", err.Error())
178 | }
179 |
180 | var sites pio.SiteFile
181 | err = json.Unmarshal(siteFileContents, &sites)
182 | if err != nil {
183 | log.Fatalf("Could not unmarshal site file contents: %s", err.Error())
184 | }
185 |
186 | for _, s := range sites {
187 | slashIndex := strings.Index(string(s.Name), "/")
188 | group := ""
189 | if slashIndex > 0 {
190 | group = string(s.Name[:slashIndex])
191 | }
192 | name := s.Name[slashIndex+1:]
193 | pass := s.PassSealed
194 | pubKey := s.PubKey
195 | isFile := s.IsFile
196 | filename := s.FileName
197 | si := pio.SiteInfo{
198 | Name: name,
199 | PassSealed: pass,
200 | PubKey: pubKey,
201 | IsFile: isFile,
202 | FileName: filename,
203 | }
204 | if st == One {
205 | if name == searchFor || fmt.Sprintf("%s/%s", group, name) == searchFor {
206 | return map[string][]pio.SiteInfo{
207 | group: []pio.SiteInfo{
208 | si,
209 | },
210 | }, allErrors
211 | }
212 | } else if st == All {
213 | if allSites[group] == nil {
214 | allSites[group] = []pio.SiteInfo{}
215 | }
216 | allSites[group] = append(allSites[group], si)
217 | } else if st == Search {
218 | if strings.Contains(group, searchFor) || strings.Contains(name, searchFor) {
219 | if allSites[group] == nil {
220 | allSites[group] = []pio.SiteInfo{}
221 | }
222 | allSites[group] = append(allSites[group], si)
223 | }
224 | }
225 | }
226 | return
227 | }
228 |
--------------------------------------------------------------------------------