├── 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 | --------------------------------------------------------------------------------