From 30e514cb88d5644777ec8546fe5017d591ca17fa Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 2 Jun 2019 01:47:19 -0700 Subject: [PATCH] Refactored config management strategy for spc --- cli/commands/decrypt.go | 7 +- cli/commands/decrypt_cfg.go | 24 ++++++ cli/commands/encrypt.go | 7 +- cli/commands/init.go | 11 +-- cli/commands/root.go | 128 +++++------------------------ cli/repositories/config.go | 145 +++++++++++++++++++++++++++++++++ cli/types/types.go | 9 ++ cmd/spc/main.go | 3 +- credentials/commands/create.go | 7 +- credentials/commands/get.go | 7 +- 10 files changed, 225 insertions(+), 123 deletions(-) create mode 100644 cli/commands/decrypt_cfg.go create mode 100644 cli/repositories/config.go create mode 100644 cli/types/types.go diff --git a/cli/commands/decrypt.go b/cli/commands/decrypt.go index 73d2936..f5ef1bd 100644 --- a/cli/commands/decrypt.go +++ b/cli/commands/decrypt.go @@ -7,13 +7,13 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" + "github.com/mitchell/selfpass/cli/types" "github.com/mitchell/selfpass/credentials/commands" "github.com/mitchell/selfpass/crypto" ) -func makeDecrypt(masterpass string, cfg *viper.Viper) *cobra.Command { +func makeDecrypt(repo types.ConfigRepo) *cobra.Command { decryptCmd := &cobra.Command{ Use: "decrypt [file]", Short: "Decrypt a file using your masterpass and secret key", @@ -22,6 +22,9 @@ the new file.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { + masterpass, cfg, err := repo.OpenConfig() + check(err) + file := args[0] fileout := file diff --git a/cli/commands/decrypt_cfg.go b/cli/commands/decrypt_cfg.go new file mode 100644 index 0000000..860c6b8 --- /dev/null +++ b/cli/commands/decrypt_cfg.go @@ -0,0 +1,24 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/mitchell/selfpass/cli/types" +) + +func makeDecryptCfg(repo types.ConfigRepo) *cobra.Command { + decryptCfg := &cobra.Command{ + Use: "decrypt-cfg", + Short: "Decrypt your config file", + Long: "Decrypt your config file, so you can access your private key, host, and certs.", + + Run: func(cmd *cobra.Command, args []string) { + _, _, err := repo.OpenConfig() + check(err) + + repo.DecryptConfig() + }, + } + + return decryptCfg +} diff --git a/cli/commands/encrypt.go b/cli/commands/encrypt.go index 7b9f9c9..d9be9e5 100644 --- a/cli/commands/encrypt.go +++ b/cli/commands/encrypt.go @@ -7,13 +7,13 @@ import ( "os" "github.com/spf13/cobra" - "github.com/spf13/viper" + "github.com/mitchell/selfpass/cli/types" "github.com/mitchell/selfpass/credentials/commands" "github.com/mitchell/selfpass/crypto" ) -func makeEncrypt(masterpass string, cfg *viper.Viper) *cobra.Command { +func makeEncrypt(repo types.ConfigRepo) *cobra.Command { encryptCmd := &cobra.Command{ Use: "encrypt [file]", Short: "Encrypt a file using your masterpass and secret key", @@ -22,6 +22,9 @@ new file.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { + masterpass, cfg, err := repo.OpenConfig() + check(err) + file := args[0] fileEnc := file + ".enc" diff --git a/cli/commands/init.go b/cli/commands/init.go index acf271c..d6d7a6c 100644 --- a/cli/commands/init.go +++ b/cli/commands/init.go @@ -8,13 +8,13 @@ import ( "github.com/google/uuid" "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" - "github.com/spf13/viper" "gopkg.in/AlecAivazis/survey.v1" + "github.com/mitchell/selfpass/cli/types" "github.com/mitchell/selfpass/credentials/commands" ) -func makeInit(cfg *viper.Viper) *cobra.Command { +func makeInit(repo types.ConfigRepo) *cobra.Command { initCmd := &cobra.Command{ Use: "init", Short: "This command initializes SPC for the first time", @@ -32,6 +32,7 @@ the users private key, and server certificates. (All of which will be encrypted) prompt survey.Prompt privateKey = strings.Replace(uuid.New().String(), "-", "", -1) ) + _, cfg, _ := repo.OpenConfig() prompt = &survey.Password{Message: "New master password:"} check(survey.AskOne(prompt, &masterpass, nil)) @@ -42,6 +43,8 @@ the users private key, and server certificates. (All of which will be encrypted) check(fmt.Errorf("master passwords didn't match")) } + repo.SetMasterpass(masterpass) + prompt = &survey.Input{Message: "Selfpass server address:"} check(survey.AskOne(prompt, &target, nil)) @@ -49,7 +52,7 @@ the users private key, and server certificates. (All of which will be encrypted) check(survey.AskOne(prompt, &hasPK, nil)) if hasPK { - prompt = &survey.Input{Message: "Private key:"} + prompt = &survey.Password{Message: "Private key:"} check(survey.AskOne(prompt, &privateKey, nil)) privateKey = strings.Replace(privateKey, "-", "", -1) } @@ -86,8 +89,6 @@ the users private key, and server certificates. (All of which will be encrypted) cfg.SetConfigFile(home + "/.spc.toml") fmt.Println("Wrote new config to: " + home + "/.spc.toml") } - - encryptConfig(masterpass, cfg) }, } diff --git a/cli/commands/root.go b/cli/commands/root.go index 3f7a61d..182bb3b 100644 --- a/cli/commands/root.go +++ b/cli/commands/root.go @@ -3,21 +3,18 @@ package commands import ( "context" "fmt" - "io/ioutil" "os" - "strings" - "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" - "github.com/spf13/viper" - "gopkg.in/AlecAivazis/survey.v1" + "github.com/mitchell/selfpass/cli/repositories" + "github.com/mitchell/selfpass/cli/types" "github.com/mitchell/selfpass/credentials/commands" + credrepos "github.com/mitchell/selfpass/credentials/repositories" credtypes "github.com/mitchell/selfpass/credentials/types" - "github.com/mitchell/selfpass/crypto" ) -func Execute(initClient credtypes.CredentialClientInit) { +func Execute() { rootCmd := &cobra.Command{ Use: "spc", Short: "This is the CLI client for Selfpass.", @@ -25,42 +22,31 @@ func Execute(initClient credtypes.CredentialClientInit) { can interact with the entire Selfpass API.`, Version: "v0.1.0", } - rootCmd.InitDefaultHelpFlag() - rootCmd.InitDefaultVersionFlag() cfgFile := rootCmd.PersistentFlags().String("config", "", "config file (default is $HOME/.spc.toml)") - decryptCfg := rootCmd.Flags().Bool("decrypt-cfg", false, "decrypt config file") - check(rootCmd.ParseFlags(os.Args)) - encryptCfg := !*decryptCfg - var masterpass string - var cfg *viper.Viper - needsCfg := (len(os.Args) > 1 && !strings.Contains(strings.Join(os.Args, "--"), "--help")) || *decryptCfg + mgr := repositories.NewConfigManager(cfgFile) + defer mgr.CloseConfig() - if needsCfg { - masterpass, cfg = openConfig(*cfgFile) - if encryptCfg && masterpass != "" { - defer encryptConfig(masterpass, cfg) - } - if *decryptCfg { - fmt.Println("Decrypting config file. It will auto-encrypt when you next run of spc.") - return - } - } + clientInit := credrepos.NewCredentialServiceClient - rootCmd.AddCommand(makeInit(cfg)) - rootCmd.AddCommand(makeEncrypt(masterpass, cfg)) - rootCmd.AddCommand(makeDecrypt(masterpass, cfg)) - rootCmd.AddCommand(commands.MakeList(makeInitClient(cfg, initClient))) - rootCmd.AddCommand(commands.MakeCreate(masterpass, cfg, makeInitClient(cfg, initClient))) - rootCmd.AddCommand(commands.MakeGet(masterpass, cfg, makeInitClient(cfg, initClient))) - rootCmd.AddCommand(commands.MakeDelete(makeInitClient(cfg, initClient))) + rootCmd.AddCommand(makeInit(mgr)) + rootCmd.AddCommand(makeEncrypt(mgr)) + rootCmd.AddCommand(makeDecrypt(mgr)) + rootCmd.AddCommand(makeDecryptCfg(mgr)) + rootCmd.AddCommand(commands.MakeList(makeInitClient(mgr, clientInit))) + rootCmd.AddCommand(commands.MakeCreate(mgr, makeInitClient(mgr, clientInit))) + rootCmd.AddCommand(commands.MakeGet(mgr, makeInitClient(mgr, clientInit))) + rootCmd.AddCommand(commands.MakeDelete(makeInitClient(mgr, clientInit))) check(rootCmd.Execute()) } -func makeInitClient(cfg *viper.Viper, initClient credtypes.CredentialClientInit) commands.CredentialClientInit { +func makeInitClient(repo types.ConfigRepo, initClient credtypes.CredentialClientInit) commands.CredentialClientInit { return func(ctx context.Context) credtypes.CredentialClient { + _, cfg, err := repo.OpenConfig() + check(err) + connConfig := cfg.GetStringMapString(keyConnConfig) client, err := initClient( @@ -70,86 +56,12 @@ func makeInitClient(cfg *viper.Viper, initClient credtypes.CredentialClientInit) connConfig["cert"], connConfig["key"], ) - if err != nil { - fmt.Printf("Please run 'init' command before running API commands.\nError Message: %s\n", err) - os.Exit(1) - } + check(err) return client } } -func openConfig(cfgFile string) (masterpass string, v *viper.Viper) { - v = viper.New() - v.SetConfigType("toml") - - if cfgFile != "" { - // Use config file from the flag. - v.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := homedir.Dir() - check(err) - - // Search config in home directory with name ".spc" (without extension). - v.AddConfigPath(home) - v.SetConfigName(".spc") - - cfgFile = home + "/.spc.toml" - } - - if _, err := os.Open(cfgFile); !os.IsNotExist(err) { - prompt := &survey.Password{Message: "Master password:"} - check(survey.AskOne(prompt, &masterpass, nil)) - - decryptConfig(masterpass, cfgFile) - } - - //v.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err := v.ReadInConfig(); err == nil { - fmt.Println("Using config file:", v.ConfigFileUsed()) - } - - return masterpass, v -} - -func decryptConfig(masterpass string, cfgFile string) { - contents, err := ioutil.ReadFile(cfgFile) - check(err) - - passkey, err := crypto.GenerateKeyFromPassword([]byte(masterpass)) - check(err) - - contents, err = crypto.CBCDecrypt(passkey, contents) - if err != nil && err.Error() == "Padding incorrect" { - fmt.Println("incorrect master password") - os.Exit(1) - } else if err != nil && err.Error() == "ciphertext is not a multiple of the block size" { - fmt.Println("Config wasn't encrypted.") - return - } - check(err) - - check(ioutil.WriteFile(cfgFile, contents, 0600)) -} - -func encryptConfig(masterpass string, cfg *viper.Viper) { - contents, err := ioutil.ReadFile(cfg.ConfigFileUsed()) - if os.IsNotExist(err) { - return - } - - keypass, err := crypto.GenerateKeyFromPassword([]byte(masterpass)) - check(err) - - contents, err = crypto.CBCEncrypt(keypass, contents) - check(err) - - check(ioutil.WriteFile(cfg.ConfigFileUsed(), contents, 0600)) -} - func check(err error) { if err != nil { fmt.Println(err) diff --git a/cli/repositories/config.go b/cli/repositories/config.go new file mode 100644 index 0000000..6d68375 --- /dev/null +++ b/cli/repositories/config.go @@ -0,0 +1,145 @@ +package repositories + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" + "gopkg.in/AlecAivazis/survey.v1" + + "github.com/mitchell/selfpass/crypto" +) + +func NewConfigManager(cfgFile *string) *ConfigManager { + return &ConfigManager{ + cfgFile: cfgFile, + } +} + +type ConfigManager struct { + masterpass string + decrypted bool + decrypt bool + cfgFile *string + v *viper.Viper +} + +func (mgr *ConfigManager) SetMasterpass(masterpass string) { + mgr.masterpass = masterpass +} + +func (mgr *ConfigManager) OpenConfig() (output string, v *viper.Viper, err error) { + if mgr.masterpass != "" { + return mgr.masterpass, mgr.v, nil + } + cfg := *mgr.cfgFile + + v = viper.New() + mgr.v = v + + v.SetConfigType("toml") + + if cfg != "" { + // Use config file from the flag. + v.SetConfigFile(cfg) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + return output, nil, err + } + + // Search config in home directory with name ".spc" (without extension). + v.AddConfigPath(home) + v.SetConfigName(".spc") + + cfg = home + "/.spc.toml" + } + + if _, err := os.Open(cfg); !os.IsNotExist(err) { + prompt := &survey.Password{Message: "Master password:"} + if err = survey.AskOne(prompt, &mgr.masterpass, nil); err != nil { + return output, nil, err + } + + mgr.decrypted, err = decryptConfig(mgr.masterpass, cfg) + if err != nil { + return output, nil, err + } + } + + // v.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err = v.ReadInConfig(); err != nil { + mgr.decrypted = true + return output, v, fmt.Errorf("no config found, run 'init' command") + } + + return mgr.masterpass, mgr.v, nil +} + +func decryptConfig(masterpass string, cfgFile string) (decrypted bool, err error) { + contents, err := ioutil.ReadFile(cfgFile) + if err != nil { + return decrypted, err + } + + passkey, err := crypto.GenerateKeyFromPassword([]byte(masterpass)) + if err != nil { + return decrypted, err + } + + contents, err = crypto.CBCDecrypt(passkey, contents) + if err != nil && err.Error() == "Padding incorrect" { + return decrypted, fmt.Errorf("incorrect master password") + } else if err != nil && err.Error() == "ciphertext is not a multiple of the block size" { + fmt.Println("Config wasn't encrypted.") + return true, nil + } + if err != nil { + return decrypted, err + } + + if err = ioutil.WriteFile(cfgFile, contents, 0600); err != nil { + return decrypted, err + } + + return true, nil +} + +func (mgr *ConfigManager) DecryptConfig() { + mgr.decrypt = true +} + +func (mgr *ConfigManager) CloseConfig() { + if !mgr.decrypt && mgr.decrypted { + contents, err := ioutil.ReadFile(mgr.v.ConfigFileUsed()) + if os.IsNotExist(err) { + return + } + + keypass, err := crypto.GenerateKeyFromPassword([]byte(mgr.masterpass)) + if err != nil { + panic(err) + } + + contents, err = crypto.CBCEncrypt(keypass, contents) + if err != nil { + panic(err) + } + + err = ioutil.WriteFile(mgr.v.ConfigFileUsed(), contents, 0600) + if err != nil { + panic(err) + } + + return + } + + if mgr.decrypt { + fmt.Println("Decrypting config file. It will auto-encrypt when you next run spc.") + } +} diff --git a/cli/types/types.go b/cli/types/types.go new file mode 100644 index 0000000..e0b5546 --- /dev/null +++ b/cli/types/types.go @@ -0,0 +1,9 @@ +package types + +import "github.com/spf13/viper" + +type ConfigRepo interface { + OpenConfig() (masterpass string, cfg *viper.Viper, err error) + DecryptConfig() + SetMasterpass(masterpass string) +} diff --git a/cmd/spc/main.go b/cmd/spc/main.go index 907ce35..d7061fe 100644 --- a/cmd/spc/main.go +++ b/cmd/spc/main.go @@ -2,9 +2,8 @@ package main import ( "github.com/mitchell/selfpass/cli/commands" - "github.com/mitchell/selfpass/credentials/repositories" ) func main() { - commands.Execute(repositories.NewCredentialServiceClient) + commands.Execute() } diff --git a/credentials/commands/create.go b/credentials/commands/create.go index bf8f839..55e0c86 100644 --- a/credentials/commands/create.go +++ b/credentials/commands/create.go @@ -11,14 +11,14 @@ import ( "github.com/atotto/clipboard" "github.com/pquerna/otp/totp" "github.com/spf13/cobra" - "github.com/spf13/viper" "gopkg.in/AlecAivazis/survey.v1" + clitypes "github.com/mitchell/selfpass/cli/types" "github.com/mitchell/selfpass/credentials/types" "github.com/mitchell/selfpass/crypto" ) -func MakeCreate(masterpass string, cfg *viper.Viper, initClient CredentialClientInit) *cobra.Command { +func MakeCreate(repo clitypes.ConfigRepo, initClient CredentialClientInit) *cobra.Command { var length uint var numbers bool var specials bool @@ -30,6 +30,9 @@ func MakeCreate(masterpass string, cfg *viper.Viper, initClient CredentialClient password.`, Run: func(_ *cobra.Command, args []string) { + masterpass, cfg, err := repo.OpenConfig() + check(err) + mdqs := []*survey.Question{ { Name: "primary", diff --git a/credentials/commands/get.go b/credentials/commands/get.go index e211117..f0444bf 100644 --- a/credentials/commands/get.go +++ b/credentials/commands/get.go @@ -10,14 +10,14 @@ import ( "github.com/atotto/clipboard" "github.com/pquerna/otp/totp" "github.com/spf13/cobra" - "github.com/spf13/viper" "gopkg.in/AlecAivazis/survey.v1" + clitypes "github.com/mitchell/selfpass/cli/types" "github.com/mitchell/selfpass/credentials/types" "github.com/mitchell/selfpass/crypto" ) -func MakeGet(masterpass string, cfg *viper.Viper, initClient CredentialClientInit) *cobra.Command { +func MakeGet(repo clitypes.ConfigRepo, initClient CredentialClientInit) *cobra.Command { getCmd := &cobra.Command{ Use: "get", Short: "Get a credential info and copy password to clipboard", @@ -29,6 +29,9 @@ decrypting password.`, defer cancel() client := initClient(ctx) + masterpass, cfg, err := repo.OpenConfig() + check(err) + mdch, errch := client.GetAllMetadata(ctx, "") mds := map[string][]types.Metadata{}