diff --git a/.gitignore b/.gitignore index 9506f47..a6d9342 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .idea bin certs -db +data +*.tar +*.enc diff --git a/Dockerfile b/Dockerfile index 90d5513..d2e36e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,10 @@ FROM golang:1.11.5 as build WORKDIR /go/src/github.com/mitchell/selfpass COPY . . ENV GO111MODULE=on +RUN make gen-certs-go RUN make build -FROM debian:stable-20190326-slim +FROM debian:stable-20190506-slim WORKDIR /usr/bin COPY --from=build /go/src/github.com/mitchell/selfpass/bin/server . ENTRYPOINT ["server"] diff --git a/Makefile b/Makefile index d0c0d1b..216c3b3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ .PHONY: all build clean format test docker-build -build: clean format test - go build --o ./bin/server ./cmd/server/server.go +build: clean format + env CGO_ENABLED=0 go build -o ./bin/server ./cmd/server + rm ./cmd/server/certs.go clean: rm -rf ./bin @@ -10,9 +11,32 @@ clean: docker: docker-compose build -start: +up: docker-compose up +upd: + docker-compose up -d + +down: + docker-compose down + +machine-create-google: + docker-machine create --driver google \ + --google-address m-selfpass \ + --google-project selfpass-241808 \ + --google-machine-type n1-standard-1 \ + --google-machine-image https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-9-stretch-v20190514 \ + selfpass01 + +machine-rm: + docker-machine rm selfpass01 + +machine-ssh: + docker-machine ssh selfpass01 + +machine-env: + docker-machine env selfpass01 + format: gofmt -w -s -l . @@ -36,5 +60,8 @@ gen-server-cert: gen-client-cert: cd certs && cfssl gencert -ca ca.pem -ca-key ca-key.pem csr.json | cfssljson -bare client +gen-certs-go: + ./gen_certs_go.sh > ./cmd/server/certs.go + test: go test -cover ./... diff --git a/cli/commands/decrypt.go b/cli/commands/decrypt.go new file mode 100644 index 0000000..73d2936 --- /dev/null +++ b/cli/commands/decrypt.go @@ -0,0 +1,52 @@ +package commands + +import ( + "encoding/hex" + "fmt" + "io/ioutil" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/mitchell/selfpass/credentials/commands" + "github.com/mitchell/selfpass/crypto" +) + +func makeDecrypt(masterpass string, cfg *viper.Viper) *cobra.Command { + decryptCmd := &cobra.Command{ + Use: "decrypt [file]", + Short: "Decrypt a file using your masterpass and secret key", + Long: `Decrypt a file using your masterpass and secret key, and replace the old file with +the new file.`, + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + file := args[0] + fileout := file + + if file[len(file)-4:] == ".enc" { + fileout = file[:len(file)-4] + } + + contents, err := ioutil.ReadFile(file) + check(err) + + key, err := hex.DecodeString(cfg.GetString(commands.KeyPrivateKey)) + check(err) + + passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key)) + check(err) + + contents, err = crypto.CBCDecrypt(passkey, contents) + check(err) + + check(ioutil.WriteFile(fileout, contents, 0600)) + check(os.Remove(file)) + + fmt.Println("Decrypted file: ", fileout) + }, + } + + return decryptCmd +} diff --git a/cli/commands/encrypt.go b/cli/commands/encrypt.go new file mode 100644 index 0000000..7b9f9c9 --- /dev/null +++ b/cli/commands/encrypt.go @@ -0,0 +1,48 @@ +package commands + +import ( + "encoding/hex" + "fmt" + "io/ioutil" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/mitchell/selfpass/credentials/commands" + "github.com/mitchell/selfpass/crypto" +) + +func makeEncrypt(masterpass string, cfg *viper.Viper) *cobra.Command { + encryptCmd := &cobra.Command{ + Use: "encrypt [file]", + Short: "Encrypt a file using your masterpass and secret key", + Long: `Encrypt a file using your masterpass and secret key, and replace the old file with the +new file.`, + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + file := args[0] + fileEnc := file + ".enc" + + contents, err := ioutil.ReadFile(file) + check(err) + + key, err := hex.DecodeString(cfg.GetString(commands.KeyPrivateKey)) + check(err) + + passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key)) + check(err) + + contents, err = crypto.CBCEncrypt(passkey, contents) + check(err) + + check(ioutil.WriteFile(fileEnc, contents, 0600)) + check(os.Remove(file)) + + fmt.Println("Encrypted file: ", fileEnc) + }, + } + + return encryptCmd +} diff --git a/cmd/spc/cmd/init.go b/cli/commands/init.go similarity index 89% rename from cmd/spc/cmd/init.go rename to cli/commands/init.go index 6d10dc3..acf271c 100644 --- a/cmd/spc/cmd/init.go +++ b/cli/commands/init.go @@ -1,4 +1,4 @@ -package cmd +package commands import ( "fmt" @@ -11,10 +11,10 @@ import ( "github.com/spf13/viper" "gopkg.in/AlecAivazis/survey.v1" - "github.com/mitchell/selfpass/credentials/cmds" + "github.com/mitchell/selfpass/credentials/commands" ) -func makeInitCmd(cfg *viper.Viper) *cobra.Command { +func makeInit(cfg *viper.Viper) *cobra.Command { initCmd := &cobra.Command{ Use: "init", Short: "This command initializes SPC for the first time", @@ -33,7 +33,7 @@ the users private key, and server certificates. (All of which will be encrypted) privateKey = strings.Replace(uuid.New().String(), "-", "", -1) ) - prompt = &survey.Password{Message: "Master password:"} + prompt = &survey.Password{Message: "New master password:"} check(survey.AskOne(prompt, &masterpass, nil)) prompt = &survey.Password{Message: "Confirm master password:"} @@ -69,14 +69,14 @@ the users private key, and server certificates. (All of which will be encrypted) key, err := ioutil.ReadFile(keyFile) check(err) - cfg.Set(cmds.KeyConnConfig, map[string]string{ + cfg.Set(keyConnConfig, map[string]string{ "target": target, "ca": string(ca), "cert": string(cert), "key": string(key), }) - cfg.Set(cmds.KeyPrivateKey, privateKey) + cfg.Set(commands.KeyPrivateKey, privateKey) if err := cfg.WriteConfig(); err != nil { home, err := homedir.Dir() diff --git a/cmd/spc/cmd/root.go b/cli/commands/root.go similarity index 64% rename from cmd/spc/cmd/root.go rename to cli/commands/root.go index 0768f0f..3f7a61d 100644 --- a/cmd/spc/cmd/root.go +++ b/cli/commands/root.go @@ -1,56 +1,67 @@ -package cmd +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/credentials/cmds" - "github.com/mitchell/selfpass/credentials/types" + "github.com/mitchell/selfpass/credentials/commands" + credtypes "github.com/mitchell/selfpass/credentials/types" "github.com/mitchell/selfpass/crypto" ) -func Execute(ctx context.Context, initClient types.CredentialClientInit) { +func Execute(initClient credtypes.CredentialClientInit) { rootCmd := &cobra.Command{ Use: "spc", Short: "This is the CLI client for Selfpass.", Long: `This is the CLI client for Selfpass, the self-hosted password manager. With this tool you 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)") - rootCmd.PersistentFlags().Parse(os.Args) - - decryptCfg := rootCmd.Flags().Bool("decrypt-cfg", false, "unencrypt config file") - rootCmd.Flags().Parse(os.Args) - + decryptCfg := rootCmd.Flags().Bool("decrypt-cfg", false, "decrypt config file") + check(rootCmd.ParseFlags(os.Args)) encryptCfg := !*decryptCfg - 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 + + var masterpass string + var cfg *viper.Viper + needsCfg := (len(os.Args) > 1 && !strings.Contains(strings.Join(os.Args, "--"), "--help")) || *decryptCfg + + 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 + } } - rootCmd.AddCommand(makeInitCmd(cfg)) - rootCmd.AddCommand(cmds.MakeListCmd(makeInitClient(cfg, initClient))) - rootCmd.AddCommand(cmds.MakeCreateCmd(masterpass, cfg, makeInitClient(cfg, initClient))) - rootCmd.AddCommand(cmds.MakeGetCmd(masterpass, cfg, makeInitClient(cfg, initClient))) + 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))) check(rootCmd.Execute()) } -func makeInitClient(cfg *viper.Viper, initClient types.CredentialClientInit) cmds.CredentialClientInit { - return func(ctx context.Context) types.CredentialClient { - connConfig := cfg.GetStringMapString(cmds.KeyConnConfig) +func makeInitClient(cfg *viper.Viper, initClient credtypes.CredentialClientInit) commands.CredentialClientInit { + return func(ctx context.Context) credtypes.CredentialClient { + connConfig := cfg.GetStringMapString(keyConnConfig) client, err := initClient( ctx, @@ -145,3 +156,5 @@ func check(err error) { os.Exit(1) } } + +const keyConnConfig = "connection" diff --git a/cmd/server/server.go b/cmd/server/server.go index c63c4d8..fcc6b49 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "flag" "io" - "io/ioutil" stdlog "log" "net" "os" @@ -28,13 +27,10 @@ var logger log.Logger func main() { var ( - stop = make(chan os.Signal, 1) - dev = flag.Bool("dev", false, "enables dev mode logging") - port = flag.String("port", "8080", "specify the port to listen on") - crtFile = flag.String("cert", "/run/secrets/server_cert", "specify the cert file") - keyFile = flag.String("key", "/run/secrets/server_key", "specify the private key file") - caFile = flag.String("ca", "/run/secrets/ca_cert", "specify the ca cert file") - verbose = flag.Bool("v", false, "be more verbose") + stop = make(chan os.Signal, 1) + jsonLogs = flag.Bool("json-logs", false, "enables json logging") + port = flag.String("port", "8080", "specify the port to listen on") + verbose = flag.Bool("v", false, "be more verbose") // tableName = flag.String( // "credential-table-name", // "selfpass-credential", @@ -47,16 +43,13 @@ func main() { signal.Notify(stop, syscall.SIGKILL) signal.Notify(stop, syscall.SIGTERM) - logger = newLogger(os.Stdout, *dev) + logger = newLogger(os.Stdout, *jsonLogs) - keypair, err := tls.LoadX509KeyPair(*crtFile, *keyFile) - check(err) - - ca, err := ioutil.ReadFile(*caFile) + keypair, err := tls.X509KeyPair([]byte(cert), []byte(key)) check(err) caPool := x509.NewCertPool() - caPool.AppendCertsFromPEM(ca) + caPool.AppendCertsFromPEM([]byte(ca)) creds := credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{keypair}, @@ -85,7 +78,6 @@ func main() { _ = logger.Log( "message", "serving", "address", addr, - "dev", dev, ) go func() { check(srv.Serve(lis)) }() @@ -95,13 +87,11 @@ func main() { srv.GracefulStop() } -func newLogger(writer io.Writer, dev bool) log.Logger { - var l log.Logger +func newLogger(writer io.Writer, jsonLogs bool) log.Logger { writer = log.NewSyncWriter(writer) + l := log.NewLogfmtLogger(writer) - if dev { - l = log.NewLogfmtLogger(writer) - } else { + if jsonLogs { l = log.NewJSONLogger(writer) } l = log.WithPrefix(l, "caller", log.Caller(5), "timestamp", log.DefaultTimestamp) diff --git a/cmd/spc/main.go b/cmd/spc/main.go index f0b3a75..907ce35 100644 --- a/cmd/spc/main.go +++ b/cmd/spc/main.go @@ -1,13 +1,10 @@ package main import ( - "context" - - "github.com/mitchell/selfpass/cmd/spc/cmd" + "github.com/mitchell/selfpass/cli/commands" "github.com/mitchell/selfpass/credentials/repositories" ) func main() { - ctx := context.Background() - cmd.Execute(ctx, repositories.NewCredentialServiceClient) + commands.Execute(repositories.NewCredentialServiceClient) } diff --git a/credentials/cmds/get.go b/credentials/cmds/get.go deleted file mode 100644 index fd7462c..0000000 --- a/credentials/cmds/get.go +++ /dev/null @@ -1,63 +0,0 @@ -package cmds - -import ( - "context" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "time" - - "github.com/atotto/clipboard" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/mitchell/selfpass/crypto" -) - -func MakeGetCmd(masterpass string, cfg *viper.Viper, initClient CredentialClientInit) *cobra.Command { - getCmd := &cobra.Command{ - Use: "get [id]", - Short: "Get a credential info and copy password to clipboard", - Long: `Get a credential's info and copy password to clipboard, from Selfpass server, after -decrypting password.`, - Args: cobra.ExactArgs(1), - - Run: func(cmd *cobra.Command, args []string) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) - defer cancel() - - cbcontents, err := clipboard.ReadAll() - check(err) - - restore := func(cbcontents string) { - time.Sleep(time.Second * 5) - clipboard.WriteAll(cbcontents) - } - - cred, err := initClient(ctx).Get(ctx, args[0]) - check(err) - - key, err := hex.DecodeString(cfg.GetString(KeyPrivateKey)) - check(err) - - passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), key) - check(err) - - passbytes, err := base64.StdEncoding.DecodeString(cred.Password) - check(err) - - plainpass, err := crypto.CBCDecrypt(passkey, passbytes) - - check(clipboard.WriteAll(string(plainpass))) - go restore(cbcontents) - - cjson, err := json.MarshalIndent(cred, "", " ") - check(err) - fmt.Println(string(cjson)) - fmt.Println("Wrote password to clipboard.") - }, - } - - return getCmd -} diff --git a/credentials/cmds/cmds.go b/credentials/commands/commands.go similarity index 85% rename from credentials/cmds/cmds.go rename to credentials/commands/commands.go index 63f58e4..8c8fadc 100644 --- a/credentials/cmds/cmds.go +++ b/credentials/commands/commands.go @@ -1,4 +1,4 @@ -package cmds +package commands import ( "context" @@ -17,5 +17,4 @@ func check(err error) { } } -const KeyConnConfig = "connection" const KeyPrivateKey = "private_key" diff --git a/credentials/cmds/create.go b/credentials/commands/create.go similarity index 60% rename from credentials/cmds/create.go rename to credentials/commands/create.go index 5929394..13813ba 100644 --- a/credentials/cmds/create.go +++ b/credentials/commands/create.go @@ -1,16 +1,15 @@ -package cmds +package commands import ( "context" "encoding/base64" "encoding/hex" - "encoding/json" "fmt" - "math/rand" "os" "time" "github.com/atotto/clipboard" + "github.com/pquerna/otp/totp" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/AlecAivazis/survey.v1" @@ -19,7 +18,11 @@ import ( "github.com/mitchell/selfpass/crypto" ) -func MakeCreateCmd(masterpass string, cfg *viper.Viper, initClient CredentialClientInit) *cobra.Command { +func MakeCreate(masterpass string, cfg *viper.Viper, initClient CredentialClientInit) *cobra.Command { + var length uint + var numbers bool + var specials bool + createCmd := &cobra.Command{ Use: "create", Short: "Create a credential in Selfpass", @@ -56,16 +59,21 @@ password.`, }, } var ci types.CredentialInput - check(survey.Ask(mdqs, &ci.MetadataInput)) check(survey.Ask(cqs, &ci)) + key, err := hex.DecodeString(cfg.GetString(KeyPrivateKey)) + check(err) + + keypass, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key)) + check(err) + var newpass bool prompt := &survey.Confirm{Message: "Do you want a random password?", Default: true} check(survey.AskOne(prompt, &newpass, nil)) if newpass { - ci.Password = generatePassword(16, true, true) + ci.Password = crypto.GeneratePassword(int(length), numbers, specials) var copypass bool prompt = &survey.Confirm{Message: "Copy new pass to clipboard?", Default: true} @@ -79,7 +87,7 @@ password.`, check(survey.AskOne(prompt, &ci.Password, nil)) var cpass string - prompt = &survey.Password{Message: "Confirm assword: "} + prompt = &survey.Password{Message: "Confirm password: "} check(survey.AskOne(prompt, &cpass, nil)) if ci.Password != cpass { @@ -88,59 +96,58 @@ password.`, } } - key, err := hex.DecodeString(cfg.GetString(KeyPrivateKey)) - check(err) - - keypass, err := crypto.CombinePasswordAndKey([]byte(masterpass), []byte(key)) - check(err) - cipherpass, err := crypto.CBCEncrypt(keypass, []byte(ci.Password)) check(err) ci.Password = base64.StdEncoding.EncodeToString(cipherpass) + var otp bool + prompt = &survey.Confirm{Message: "Do you have an OTP/MFA secret?", Default: true} + check(survey.AskOne(prompt, &otp, nil)) + + if otp { + var secret string + prompt := &survey.Password{Message: "OTP secret:"} + check(survey.AskOne(prompt, &secret, nil)) + + ciphersecret, err := crypto.CBCEncrypt(keypass, []byte(secret)) + check(err) + + ci.OTPSecret = base64.StdEncoding.EncodeToString(ciphersecret) + + var copyotp bool + prompt2 := &survey.Confirm{Message: "Copy new OTP to clipboard?", Default: true} + check(survey.AskOne(prompt2, ©otp, nil)) + + if copyotp { + otp, err := totp.GenerateCode(secret, time.Now()) + check(err) + + check(clipboard.WriteAll(otp)) + } + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() c, err := initClient(ctx).Create(ctx, ci) check(err) - mdjson, err := json.MarshalIndent(c.Metadata, "", " ") - check(err) - fmt.Println(string(mdjson)) + fmt.Println(c) + + var cleancb bool + prompt = &survey.Confirm{Message: "Do you want to clear the clipboard?", Default: true} + check(survey.AskOne(prompt, &cleancb, nil)) + + if cleancb { + check(clipboard.WriteAll(" ")) + } }, } + createCmd.Flags().BoolVarP(&numbers, "numbers", "n", true, "use numbers in the generated password") + createCmd.Flags().BoolVarP(&specials, "specials", "s", false, "use special characters in the generated password") + createCmd.Flags().UintVarP(&length, "length", "l", 32, "length of the generated password") + return createCmd } - -const alphas = "abcdefghijklmnopqrstuvABCDEFGHIJKLMNOPQRSTUV" -const alphanumerics = "abcdefghijklmnopqrstuvABCDEFGHIJKLMNOPQRSTUV1234567890" -const alphasAndSpecials = "abcdefghijklmnopqrstuvABCDEFGHIJKLMNOPQRSTUV1234567890!@#$%^&*()" -const alphanumericsAndSpecials = "abcdefghijklmnopqrstuvABCDEFGHIJKLMNOPQRSTUV1234567890!@#$%^&*()" - -func generatePassword(length int, numbers, specials bool) string { - rand.Seed(time.Now().UnixNano()) - pass := make([]byte, length) - - switch { - case numbers && specials: - for idx := 0; idx < length; idx++ { - pass[idx] = alphanumericsAndSpecials[rand.Int63()%int64(len(alphanumericsAndSpecials))] - } - case numbers: - for idx := 0; idx < length; idx++ { - pass[idx] = alphanumerics[rand.Int63()%int64(len(alphanumerics))] - } - case specials: - for idx := 0; idx < length; idx++ { - pass[idx] = alphasAndSpecials[rand.Int63()%int64(len(alphasAndSpecials))] - } - default: - for idx := 0; idx < length; idx++ { - pass[idx] = alphas[rand.Int63()%int64(len(alphas))] - } - } - - return string(pass) -} diff --git a/credentials/commands/delete.go b/credentials/commands/delete.go new file mode 100644 index 0000000..18410a7 --- /dev/null +++ b/credentials/commands/delete.go @@ -0,0 +1,33 @@ +package commands + +import ( + "context" + "time" + + "github.com/spf13/cobra" + "gopkg.in/AlecAivazis/survey.v1" +) + +func MakeDelete(initConfig CredentialClientInit) *cobra.Command { + deleteCmd := &cobra.Command{ + Use: "delete [id]", + Short: "Delete a credential using the given ID", + Long: `Delete a credential using the given ID, permanently. THERE IS NO UNDOING THIS ACTION.`, + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + var confirmed bool + prompt := &survey.Confirm{Message: "Are you sure you want to permanently delete this credential?"} + check(survey.AskOne(prompt, &confirmed, nil)) + + if confirmed { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + check(initConfig(ctx).Delete(ctx, args[0])) + } + }, + } + + return deleteCmd +} diff --git a/credentials/commands/get.go b/credentials/commands/get.go new file mode 100644 index 0000000..3eb5e5b --- /dev/null +++ b/credentials/commands/get.go @@ -0,0 +1,92 @@ +package commands + +import ( + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "time" + + "github.com/atotto/clipboard" + "github.com/pquerna/otp/totp" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/AlecAivazis/survey.v1" + + "github.com/mitchell/selfpass/crypto" +) + +func MakeGet(masterpass string, cfg *viper.Viper, initClient CredentialClientInit) *cobra.Command { + getCmd := &cobra.Command{ + Use: "get [id]", + Short: "Get a credential info and copy password to clipboard", + Long: `Get a credential's info and copy password to clipboard, from Selfpass server, after +decrypting password.`, + Args: cobra.ExactArgs(1), + + Run: func(cmd *cobra.Command, args []string) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + cred, err := initClient(ctx).Get(ctx, args[0]) + check(err) + + fmt.Println(cred) + + check(clipboard.WriteAll(string(cred.Primary))) + + fmt.Println("Wrote primary user key to clipboard.") + + key, err := hex.DecodeString(cfg.GetString(KeyPrivateKey)) + check(err) + + passkey, err := crypto.CombinePasswordAndKey([]byte(masterpass), key) + check(err) + + var copyPass bool + prompt := &survey.Confirm{Message: "Copy password to clipboard?", Default: true} + check(survey.AskOne(prompt, ©Pass, nil)) + + if copyPass { + passbytes, err := base64.StdEncoding.DecodeString(cred.Password) + check(err) + + plainpass, err := crypto.CBCDecrypt(passkey, passbytes) + + check(clipboard.WriteAll(string(plainpass))) + + fmt.Println("Wrote password to clipboard.") + } + + if cred.OTPSecret != "" { + var newOTP bool + prompt = &survey.Confirm{Message: "Generate one time password and copy to clipboard?", Default: true} + check(survey.AskOne(prompt, &newOTP, nil)) + + if newOTP { + secretbytes, err := base64.StdEncoding.DecodeString(cred.OTPSecret) + check(err) + + plainsecret, err := crypto.CBCDecrypt(passkey, secretbytes) + + otp, err := totp.GenerateCode(string(plainsecret), time.Now()) + check(err) + + check(clipboard.WriteAll(otp)) + + fmt.Println("Wrote one time password to clipboard.") + } + } + + var cleancb bool + prompt = &survey.Confirm{Message: "Do you want to clear the clipboard?", Default: true} + check(survey.AskOne(prompt, &cleancb, nil)) + + if cleancb { + check(clipboard.WriteAll(" ")) + } + }, + } + + return getCmd +} diff --git a/credentials/cmds/list.go b/credentials/commands/list.go similarity index 82% rename from credentials/cmds/list.go rename to credentials/commands/list.go index 84999fa..1b5f17b 100644 --- a/credentials/cmds/list.go +++ b/credentials/commands/list.go @@ -1,15 +1,14 @@ -package cmds +package commands import ( "context" - "encoding/json" "fmt" "time" "github.com/spf13/cobra" ) -func MakeListCmd(initClient CredentialClientInit) *cobra.Command { +func MakeList(initClient CredentialClientInit) *cobra.Command { var sourceHost string listCmd := &cobra.Command{ @@ -38,11 +37,11 @@ includes almost all the information but the most sensitive.`, break receive } - mdjson, err := json.MarshalIndent(md, "", " ") - check(err) - fmt.Println(string(mdjson)) + fmt.Println(md) } } + + fmt.Println("Done listing.") }, } diff --git a/credentials/protobuf/service.pb.go b/credentials/protobuf/service.pb.go index 51b31a6..62e6311 100644 --- a/credentials/protobuf/service.pb.go +++ b/credentials/protobuf/service.pb.go @@ -357,6 +357,7 @@ type Credential struct { SourceHost string `protobuf:"bytes,8,opt,name=source_host,json=sourceHost,proto3" json:"source_host,omitempty"` LoginUrl string `protobuf:"bytes,9,opt,name=login_url,json=loginUrl,proto3" json:"login_url,omitempty"` Tag string `protobuf:"bytes,10,opt,name=tag,proto3" json:"tag,omitempty"` + OtpSecret string `protobuf:"bytes,11,opt,name=otp_secret,json=otpSecret,proto3" json:"otp_secret,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -457,6 +458,13 @@ func (m *Credential) GetTag() string { return "" } +func (m *Credential) GetOtpSecret() string { + if m != nil { + return m.OtpSecret + } + return "" +} + type CredentialRequest struct { Primary string `protobuf:"bytes,1,opt,name=primary,proto3" json:"primary,omitempty"` Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` @@ -465,6 +473,7 @@ type CredentialRequest struct { SourceHost string `protobuf:"bytes,5,opt,name=source_host,json=sourceHost,proto3" json:"source_host,omitempty"` LoginUrl string `protobuf:"bytes,6,opt,name=login_url,json=loginUrl,proto3" json:"login_url,omitempty"` Tag string `protobuf:"bytes,7,opt,name=tag,proto3" json:"tag,omitempty"` + OtpSecret string `protobuf:"bytes,8,opt,name=otp_secret,json=otpSecret,proto3" json:"otp_secret,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -544,6 +553,13 @@ func (m *CredentialRequest) GetTag() string { return "" } +func (m *CredentialRequest) GetOtpSecret() string { + if m != nil { + return m.OtpSecret + } + return "" +} + func init() { proto.RegisterType((*DeleteResponse)(nil), "selfpass.credentials.DeleteResponse") proto.RegisterType((*GetAllMetadataRequest)(nil), "selfpass.credentials.GetAllMetadataRequest") @@ -559,41 +575,43 @@ func init() { func init() { proto.RegisterFile("credentials/protobuf/service.proto", fileDescriptor_ad34efc7bbd96e69) } var fileDescriptor_ad34efc7bbd96e69 = []byte{ - // 535 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x94, 0xcf, 0x6e, 0xd3, 0x40, - 0x10, 0xc6, 0xe5, 0xa4, 0x4d, 0xed, 0x69, 0x88, 0x60, 0x55, 0x24, 0xcb, 0x95, 0x48, 0x64, 0x90, - 0xa8, 0x8a, 0xe4, 0xa2, 0x72, 0x81, 0x63, 0x68, 0x51, 0xca, 0x01, 0x21, 0xb9, 0xf4, 0xc2, 0x25, - 0xda, 0xda, 0xd3, 0xd4, 0x92, 0xed, 0x35, 0xbb, 0x63, 0x50, 0x1f, 0x8c, 0x97, 0x80, 0x37, 0xe2, - 0x84, 0xbc, 0xfe, 0x93, 0x3f, 0x75, 0x9a, 0x48, 0x5c, 0xb8, 0xed, 0xac, 0xbf, 0x59, 0xcd, 0xef, - 0xdb, 0xf5, 0x07, 0x6e, 0x20, 0x31, 0xc4, 0x94, 0x22, 0x1e, 0xab, 0x93, 0x4c, 0x0a, 0x12, 0xd7, - 0xf9, 0xcd, 0x89, 0x42, 0xf9, 0x3d, 0x0a, 0xd0, 0xd3, 0x1b, 0xec, 0x40, 0x61, 0x7c, 0x93, 0x71, - 0xa5, 0xbc, 0x05, 0xb1, 0x33, 0x9c, 0x09, 0x31, 0x8b, 0x71, 0xde, 0x44, 0x51, 0x82, 0x8a, 0x78, - 0x92, 0x95, 0x6d, 0xee, 0x31, 0x0c, 0xce, 0x31, 0x46, 0x42, 0x1f, 0x55, 0x26, 0x52, 0x85, 0xcc, - 0x86, 0x3d, 0x95, 0x07, 0x01, 0x2a, 0x65, 0x1b, 0x23, 0xe3, 0xc8, 0xf4, 0xeb, 0xd2, 0x7d, 0x0b, - 0x4f, 0x27, 0x48, 0xe3, 0x38, 0xfe, 0x84, 0xc4, 0x43, 0x4e, 0xdc, 0xc7, 0x6f, 0x39, 0x2a, 0x62, - 0x43, 0xd8, 0x57, 0x22, 0x97, 0x01, 0x4e, 0x6f, 0x85, 0x22, 0xdd, 0x66, 0xf9, 0x50, 0x6e, 0x5d, - 0x08, 0x45, 0xee, 0x21, 0x58, 0x1f, 0xc3, 0x5a, 0x3d, 0x80, 0x4e, 0x14, 0x56, 0xa2, 0x4e, 0x14, - 0xba, 0xb7, 0xf0, 0xe8, 0x2a, 0x0b, 0x79, 0x31, 0x42, 0xab, 0x80, 0x4d, 0x00, 0xe6, 0x4c, 0x76, - 0x67, 0x64, 0x1c, 0xed, 0x9f, 0xbe, 0xf4, 0xda, 0x78, 0xbd, 0xb3, 0x66, 0x5d, 0x1d, 0xe6, 0x2f, - 0xb4, 0xba, 0xc7, 0xd0, 0x3f, 0xcf, 0x93, 0xac, 0x41, 0x75, 0xc0, 0x0c, 0x44, 0x4a, 0x98, 0x52, - 0xc9, 0xda, 0xf7, 0x9b, 0xda, 0x1d, 0x40, 0xff, 0x43, 0x92, 0xd1, 0x5d, 0x75, 0x8e, 0xfb, 0xc7, - 0x00, 0xb3, 0xe6, 0xbe, 0x37, 0xe1, 0x3b, 0x3d, 0x21, 0x27, 0x0c, 0xa7, 0x9c, 0xaa, 0x09, 0x1d, - 0xaf, 0xf4, 0xde, 0xab, 0xbd, 0xf7, 0xbe, 0xd4, 0xde, 0xfb, 0x56, 0xa5, 0x1e, 0x53, 0xd1, 0x9a, - 0x6b, 0x7a, 0xdd, 0xda, 0xdd, 0xdc, 0x5a, 0xa9, 0xc7, 0x54, 0xdc, 0x54, 0x26, 0xa3, 0x84, 0xcb, - 0x3b, 0x7b, 0x47, 0x8f, 0x52, 0x97, 0xab, 0x17, 0xb2, 0xbb, 0x7a, 0x21, 0xec, 0x10, 0xac, 0x58, - 0xcc, 0xa2, 0x74, 0x9a, 0xcb, 0xd8, 0xee, 0xe9, 0xcf, 0xa6, 0xde, 0xb8, 0x92, 0x31, 0x7b, 0x0c, - 0x5d, 0xe2, 0x33, 0x7b, 0x4f, 0x6f, 0x17, 0x4b, 0xf7, 0x57, 0x07, 0x60, 0x6e, 0xed, 0x7f, 0x8f, - 0xef, 0x80, 0x99, 0x2b, 0x94, 0x29, 0x4f, 0xb0, 0x62, 0x6f, 0x6a, 0x76, 0x00, 0xbb, 0x98, 0xf0, - 0xa8, 0xa6, 0x2e, 0x8b, 0xa2, 0xa3, 0x78, 0x4b, 0x3f, 0x84, 0x0c, 0x2b, 0xee, 0xa6, 0x5e, 0x35, - 0xd3, 0x7c, 0xd8, 0x4c, 0xab, 0xdd, 0x4c, 0x98, 0x9b, 0xf9, 0xdb, 0x80, 0x27, 0xf7, 0xde, 0xe9, - 0x22, 0x8d, 0xb1, 0x9e, 0xa6, 0xb3, 0x8e, 0xa6, 0xbb, 0x8e, 0x66, 0xe7, 0x61, 0x9a, 0x7f, 0x7d, - 0x1a, 0xa7, 0x3f, 0xbb, 0x8b, 0x34, 0x97, 0x65, 0x26, 0xb1, 0x29, 0x0c, 0x96, 0xa3, 0x82, 0xbd, - 0x6a, 0xff, 0x61, 0x5b, 0x03, 0xc5, 0x79, 0xd6, 0x2e, 0xae, 0x65, 0xaf, 0x0d, 0x76, 0x01, 0xdd, - 0x09, 0x12, 0x1b, 0xb6, 0x0b, 0x9b, 0xb0, 0x71, 0x46, 0x9b, 0x72, 0x82, 0x5d, 0x42, 0xef, 0x4c, - 0x3f, 0x47, 0xb6, 0x6d, 0xa6, 0x6c, 0x71, 0xe8, 0x67, 0xe8, 0x95, 0x99, 0xc6, 0x9e, 0xb7, 0x6b, - 0x97, 0x12, 0x6f, 0xbb, 0x03, 0xcb, 0x9c, 0xde, 0x8c, 0xfc, 0xa2, 0x5d, 0xb0, 0x1c, 0xf3, 0xef, - 0xe1, 0xab, 0x59, 0xff, 0x5e, 0xd7, 0x3d, 0xbd, 0x7a, 0xf3, 0x37, 0x00, 0x00, 0xff, 0xff, 0x22, - 0xb6, 0x48, 0x2d, 0x68, 0x06, 0x00, 0x00, + // 561 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x95, 0x4d, 0x6f, 0xd3, 0x4c, + 0x10, 0xc7, 0xe5, 0xa4, 0x4d, 0xed, 0x69, 0x9f, 0xe8, 0x61, 0x55, 0x24, 0xcb, 0x15, 0x24, 0x32, + 0x48, 0x54, 0x45, 0x72, 0x51, 0xb9, 0xc0, 0x31, 0xb4, 0x28, 0xe5, 0x80, 0x90, 0x1c, 0x7a, 0xe1, + 0x12, 0x6d, 0xed, 0x69, 0x6a, 0xc9, 0xf6, 0x9a, 0xdd, 0x31, 0xa8, 0x1f, 0x8c, 0x0f, 0x05, 0x1f, + 0x81, 0x13, 0xf2, 0xfa, 0x25, 0x2f, 0x75, 0x9a, 0x48, 0x5c, 0xb8, 0xed, 0x8c, 0xff, 0xb3, 0x9a, + 0xdf, 0xec, 0xee, 0xdf, 0xe0, 0x06, 0x12, 0x43, 0x4c, 0x29, 0xe2, 0xb1, 0x3a, 0xcd, 0xa4, 0x20, + 0x71, 0x9d, 0xdf, 0x9c, 0x2a, 0x94, 0xdf, 0xa2, 0x00, 0x3d, 0x9d, 0x60, 0x87, 0x0a, 0xe3, 0x9b, + 0x8c, 0x2b, 0xe5, 0x2d, 0x88, 0x9d, 0xc1, 0x4c, 0x88, 0x59, 0x8c, 0xf3, 0x22, 0x8a, 0x12, 0x54, + 0xc4, 0x93, 0xac, 0x2c, 0x73, 0x4f, 0xa0, 0x7f, 0x81, 0x31, 0x12, 0xfa, 0xa8, 0x32, 0x91, 0x2a, + 0x64, 0x36, 0xec, 0xa9, 0x3c, 0x08, 0x50, 0x29, 0xdb, 0x18, 0x1a, 0xc7, 0xa6, 0x5f, 0x87, 0xee, + 0x1b, 0x78, 0x3c, 0x46, 0x1a, 0xc5, 0xf1, 0x47, 0x24, 0x1e, 0x72, 0xe2, 0x3e, 0x7e, 0xcd, 0x51, + 0x11, 0x1b, 0xc0, 0xbe, 0x12, 0xb9, 0x0c, 0x70, 0x7a, 0x2b, 0x14, 0xe9, 0x32, 0xcb, 0x87, 0x32, + 0x75, 0x29, 0x14, 0xb9, 0x47, 0x60, 0x7d, 0x08, 0x6b, 0x75, 0x1f, 0x3a, 0x51, 0x58, 0x89, 0x3a, + 0x51, 0xe8, 0xde, 0xc2, 0x7f, 0x57, 0x59, 0xc8, 0x8b, 0x16, 0x5a, 0x05, 0x6c, 0x0c, 0x30, 0x67, + 0xb2, 0x3b, 0x43, 0xe3, 0x78, 0xff, 0xec, 0x85, 0xd7, 0xc6, 0xeb, 0x9d, 0x37, 0xeb, 0x6a, 0x33, + 0x7f, 0xa1, 0xd4, 0x3d, 0x81, 0x83, 0x8b, 0x3c, 0xc9, 0x1a, 0x54, 0x07, 0xcc, 0x40, 0xa4, 0x84, + 0x29, 0x95, 0xac, 0x07, 0x7e, 0x13, 0xbb, 0x7d, 0x38, 0x78, 0x9f, 0x64, 0x74, 0x57, 0xed, 0xe3, + 0xfe, 0x36, 0xc0, 0xac, 0xb9, 0xef, 0x75, 0xf8, 0x56, 0x77, 0xc8, 0x09, 0xc3, 0x29, 0xa7, 0xaa, + 0x43, 0xc7, 0x2b, 0x67, 0xef, 0xd5, 0xb3, 0xf7, 0x3e, 0xd7, 0xb3, 0xf7, 0xad, 0x4a, 0x3d, 0xa2, + 0xa2, 0x34, 0xd7, 0xf4, 0xba, 0xb4, 0xbb, 0xb9, 0xb4, 0x52, 0x8f, 0xa8, 0x38, 0xa9, 0x4c, 0x46, + 0x09, 0x97, 0x77, 0xf6, 0x8e, 0x6e, 0xa5, 0x0e, 0x57, 0x0f, 0x64, 0x77, 0xf5, 0x40, 0xd8, 0x11, + 0x58, 0xb1, 0x98, 0x45, 0xe9, 0x34, 0x97, 0xb1, 0xdd, 0xd3, 0x9f, 0x4d, 0x9d, 0xb8, 0x92, 0x31, + 0xfb, 0x1f, 0xba, 0xc4, 0x67, 0xf6, 0x9e, 0x4e, 0x17, 0x4b, 0xf7, 0x67, 0x07, 0x60, 0x3e, 0xda, + 0x7f, 0x1e, 0xdf, 0x01, 0x33, 0x57, 0x28, 0x53, 0x9e, 0x60, 0xc5, 0xde, 0xc4, 0xec, 0x10, 0x76, + 0x31, 0xe1, 0x51, 0x4d, 0x5d, 0x06, 0x45, 0x45, 0x71, 0x97, 0xbe, 0x0b, 0x19, 0x56, 0xdc, 0x4d, + 0xbc, 0x3a, 0x4c, 0xf3, 0xe1, 0x61, 0x5a, 0xed, 0xc3, 0x84, 0x66, 0x98, 0xec, 0x09, 0x80, 0xa0, + 0x6c, 0xaa, 0x30, 0x90, 0x48, 0xf6, 0xbe, 0xfe, 0x60, 0x09, 0xca, 0x26, 0x3a, 0xe1, 0xfe, 0x32, + 0xe0, 0xd1, 0xbd, 0x6b, 0xbc, 0x08, 0x6b, 0xac, 0x87, 0xed, 0xac, 0x83, 0xed, 0xae, 0x83, 0xdd, + 0x79, 0x18, 0xf6, 0x6f, 0x6f, 0xce, 0x0a, 0xac, 0xb9, 0x02, 0x7b, 0xf6, 0xa3, 0xbb, 0x08, 0x3b, + 0x29, 0x1d, 0x8d, 0x4d, 0xa1, 0xbf, 0x6c, 0x34, 0xec, 0x65, 0xfb, 0x73, 0x6f, 0xb5, 0x23, 0xe7, + 0x69, 0xbb, 0xb8, 0x96, 0xbd, 0x32, 0xd8, 0x25, 0x74, 0xc7, 0x48, 0x6c, 0xd0, 0x2e, 0x6c, 0xac, + 0xca, 0x19, 0x6e, 0x72, 0x19, 0x36, 0x81, 0xde, 0xb9, 0xbe, 0xcc, 0x6c, 0x5b, 0x47, 0xda, 0x62, + 0xd3, 0x4f, 0xd0, 0x2b, 0x1d, 0x91, 0x3d, 0x6b, 0xd7, 0x2e, 0xf9, 0xe5, 0x76, 0x1b, 0x96, 0x2e, + 0xbf, 0x19, 0xf9, 0x79, 0xbb, 0x60, 0xf9, 0x27, 0xf1, 0x0e, 0xbe, 0x98, 0xf5, 0xe3, 0xbc, 0xee, + 0xe9, 0xd5, 0xeb, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xde, 0x03, 0x4f, 0xb2, 0xa6, 0x06, 0x00, + 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/credentials/protobuf/service.proto b/credentials/protobuf/service.proto index 6ddc759..c9dfa49 100644 --- a/credentials/protobuf/service.proto +++ b/credentials/protobuf/service.proto @@ -60,6 +60,7 @@ message Credential { string source_host = 8; string login_url = 9; string tag = 10; + string otp_secret = 11; } message CredentialRequest { @@ -70,4 +71,5 @@ message CredentialRequest { string source_host = 5; string login_url = 6; string tag = 7; + string otp_secret = 8; } diff --git a/credentials/repositories/grpc_client.go b/credentials/repositories/grpc_client.go index b21273b..63f7197 100644 --- a/credentials/repositories/grpc_client.go +++ b/credentials/repositories/grpc_client.go @@ -7,12 +7,13 @@ import ( "fmt" "io" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "github.com/mitchell/selfpass/credentials/endpoints" "github.com/mitchell/selfpass/credentials/protobuf" "github.com/mitchell/selfpass/credentials/transport" "github.com/mitchell/selfpass/credentials/types" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" ) func NewCredentialServiceClient(ctx context.Context, target, ca, cert, key string) (types.CredentialClient, error) { @@ -34,16 +35,16 @@ func NewCredentialServiceClient(ctx context.Context, target, ca, cert, key strin return nil, err } - return CredentialServiceClient{ + return credentialServiceClient{ client: protobuf.NewCredentialServiceClient(conn), }, nil } -type CredentialServiceClient struct { +type credentialServiceClient struct { client protobuf.CredentialServiceClient } -func (c CredentialServiceClient) GetAllMetadata(ctx context.Context, sourceHost string) (output <-chan types.Metadata, errch chan error) { +func (c credentialServiceClient) GetAllMetadata(ctx context.Context, sourceHost string) (output <-chan types.Metadata, errch chan error) { pbmdch := make(chan protobuf.Metadata, 1) errch = make(chan error, 1) @@ -84,7 +85,7 @@ func (c CredentialServiceClient) GetAllMetadata(ctx context.Context, sourceHost return stream.Metadata, stream.Errors } -func (c CredentialServiceClient) Get(ctx context.Context, id string) (output types.Credential, err error) { +func (c credentialServiceClient) Get(ctx context.Context, id string) (output types.Credential, err error) { req := transport.EncodeIdRequest(endpoints.IDRequest{ID: id}) res, err := c.client.Get(ctx, &req) @@ -95,7 +96,7 @@ func (c CredentialServiceClient) Get(ctx context.Context, id string) (output typ return transport.DecodeCredential(*res) } -func (c CredentialServiceClient) Create(ctx context.Context, ci types.CredentialInput) (output types.Credential, err error) { +func (c credentialServiceClient) Create(ctx context.Context, ci types.CredentialInput) (output types.Credential, err error) { req := transport.EncodeCredentialRequest(ci) res, err := c.client.Create(ctx, &req) @@ -106,10 +107,26 @@ func (c CredentialServiceClient) Create(ctx context.Context, ci types.Credential return transport.DecodeCredential(*res) } -func (c CredentialServiceClient) Update(ctx context.Context, id string, ci types.CredentialInput) (output types.Credential, err error) { - panic("implement me") +func (c credentialServiceClient) Update(ctx context.Context, id string, ci types.CredentialInput) (output types.Credential, err error) { + req := transport.EncodeUpdateRequest(endpoints.UpdateRequest{ID: id, Credential: ci}) + + res, err := c.client.Update(ctx, &req) + if err != nil { + return output, err + } + + return transport.DecodeCredential(*res) } -func (c CredentialServiceClient) Delete(ctx context.Context, id string) (err error) { - panic("implement me") +func (c credentialServiceClient) Delete(ctx context.Context, id string) (err error) { + req := transport.EncodeIdRequest(endpoints.IDRequest{ID: id}) + + res, err := c.client.Delete(ctx, &req) + if err != nil { + return err + } + if !res.Success { + return fmt.Errorf("delete unsuccessful") + } + return nil } diff --git a/credentials/service/service.go b/credentials/service/service.go index 3386bea..d3ad4b9 100644 --- a/credentials/service/service.go +++ b/credentials/service/service.go @@ -46,6 +46,7 @@ func (svc Credentials) Create(ctx context.Context, ci types.CredentialInput) (ou c.Username = ci.Username c.Email = ci.Email c.Password = ci.Password + c.OTPSecret = ci.OTPSecret c.Tag = ci.Tag err = svc.repo.Put(ctx, c) @@ -97,6 +98,7 @@ func (svc Credentials) Update(ctx context.Context, id string, ci types.Credentia c.LoginURL = ci.LoginURL c.SourceHost = ci.SourceHost c.Password = ci.Password + c.OTPSecret = ci.OTPSecret c.Email = ci.Email c.Username = ci.Username c.Tag = ci.Tag diff --git a/credentials/transport/encoding.go b/credentials/transport/encoding.go index 1431186..8dd733f 100644 --- a/credentials/transport/encoding.go +++ b/credentials/transport/encoding.go @@ -4,6 +4,7 @@ import ( "context" "github.com/golang/protobuf/ptypes" + "github.com/mitchell/selfpass/credentials/endpoints" "github.com/mitchell/selfpass/credentials/protobuf" "github.com/mitchell/selfpass/credentials/types" @@ -121,9 +122,10 @@ func decodeCredentialRequest(ctx context.Context, request interface{}) (interfac SourceHost: r.SourceHost, Tag: r.Tag, }, - Username: r.Username, - Email: r.Email, - Password: r.Password, + Username: r.Username, + Email: r.Email, + Password: r.Password, + OTPSecret: r.OtpSecret, }, nil } @@ -133,6 +135,7 @@ func EncodeCredentialRequest(r types.CredentialInput) protobuf.CredentialRequest Username: r.Username, Email: r.Email, Password: r.Password, + OtpSecret: r.OTPSecret, SourceHost: r.SourceHost, LoginUrl: r.LoginURL, Tag: r.Tag, @@ -163,6 +166,7 @@ func encodeCredentialResponse(ctx context.Context, response interface{}) (interf Username: r.Username, Email: r.Email, Password: r.Password, + OtpSecret: r.OTPSecret, }, nil } @@ -188,9 +192,10 @@ func DecodeCredential(r protobuf.Credential) (c types.Credential, err error) { LoginURL: r.LoginUrl, Tag: r.Tag, }, - Username: r.Username, - Email: r.Email, - Password: r.Password, + Username: r.Username, + Email: r.Email, + Password: r.Password, + OTPSecret: r.OtpSecret, }, nil } @@ -206,16 +211,15 @@ func decodeUpdateRequest(ctx context.Context, request interface{}) (interface{}, LoginURL: r.Credential.LoginUrl, Tag: r.Credential.Tag, }, - Username: r.Credential.Username, - Email: r.Credential.Email, - Password: r.Credential.Password, + Username: r.Credential.Username, + Email: r.Credential.Email, + Password: r.Credential.Password, + OTPSecret: r.Credential.OtpSecret, }, }, nil } -func EncodeUpdateRequest(ctx context.Context, request interface{}) (interface{}, error) { - r := request.(endpoints.UpdateRequest) - +func EncodeUpdateRequest(r endpoints.UpdateRequest) protobuf.UpdateRequest { c := r.Credential return protobuf.UpdateRequest{ Id: r.ID, @@ -224,11 +228,12 @@ func EncodeUpdateRequest(ctx context.Context, request interface{}) (interface{}, Username: c.Username, Email: c.Email, Password: c.Password, + OtpSecret: c.OTPSecret, SourceHost: c.SourceHost, LoginUrl: c.LoginURL, Tag: c.Tag, }, - }, nil + } } func decodeIdRequest(ctx context.Context, request interface{}) (interface{}, error) { diff --git a/credentials/types/credential.go b/credentials/types/credential.go index dedb919..a332068 100644 --- a/credentials/types/credential.go +++ b/credentials/types/credential.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "time" ) @@ -8,16 +9,25 @@ const TypePrefixCred = "cred" type Credential struct { Metadata - Username string - Email string - Password string `json:"-"` + Username string + Email string + Password string `json:"-"` + OTPSecret string `json:"-"` +} + +func (c Credential) String() string { + return fmt.Sprintf( + "username = %s\nemail = %s\n%s", + c.Username, c.Email, c.Metadata, + ) } type CredentialInput struct { MetadataInput - Username string - Email string - Password string + Username string + Email string + Password string + OTPSecret string } type Metadata struct { @@ -30,6 +40,13 @@ type Metadata struct { Tag string } +func (m Metadata) String() string { + return fmt.Sprintf( + "id = %s\nsourceHost = %s\ncreatedAt = %s\nupdatedAt = %s\nprimary = %s\nloginUrl = %s\ntag = %s\n", + m.ID, m.SourceHost, m.CreatedAt, m.UpdatedAt, m.Primary, m.LoginURL, m.Tag, + ) +} + type MetadataInput struct { Primary string SourceHost string diff --git a/crypto/cbc.go b/crypto/cbc.go index cf644f2..6bde7c5 100644 --- a/crypto/cbc.go +++ b/crypto/cbc.go @@ -10,37 +10,6 @@ import ( "github.com/cloudflare/redoctober/padding" ) -func GenerateKeyFromPassword(pass []byte) ([]byte, error) { - if len(pass) < 8 { - return nil, fmt.Errorf("master password must be at least 8 characters") - } - - for idx := 0; len(pass) < 32; idx++ { - pass = append(pass, pass[idx]) - - if idx == len(pass) { - idx = 0 - } - } - - return pass, nil -} - -func CombinePasswordAndKey(pass, key []byte) ([]byte, error) { - if len(pass) < 8 { - return nil, fmt.Errorf("master password must be at least 8 characters") - } - if len(key) != 16 { - return nil, fmt.Errorf("key was not of length 16") - } - - for idx := 0; len(pass) < 16; idx++ { - pass = append(pass, pass[idx]) - } - - return append(pass[:16], key...), nil -} - func CBCEncrypt(key []byte, plaintext []byte) ([]byte, error) { if len(key) != 32 { return nil, fmt.Errorf("key is not 32 bytes") diff --git a/crypto/helpers.go b/crypto/helpers.go new file mode 100644 index 0000000..5dee648 --- /dev/null +++ b/crypto/helpers.go @@ -0,0 +1,68 @@ +package crypto + +import ( + "fmt" + "math/rand" + "time" +) + +func GenerateKeyFromPassword(pass []byte) ([]byte, error) { + if len(pass) < 8 { + return nil, fmt.Errorf("master password must be at least 8 characters") + } + + for idx := 0; len(pass) < 32; idx++ { + pass = append(pass, pass[idx]) + + if idx == len(pass) { + idx = 0 + } + } + + return pass, nil +} + +func CombinePasswordAndKey(pass, key []byte) ([]byte, error) { + if len(pass) < 8 { + return nil, fmt.Errorf("master password must be at least 8 characters") + } + if len(key) != 16 { + return nil, fmt.Errorf("key was not of length 16") + } + + for idx := 0; len(pass) < 16; idx++ { + pass = append(pass, pass[idx]) + } + + return append(pass[:16], key...), nil +} + +func GeneratePassword(length int, numbers, specials bool) string { + const alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + const alphanumerics = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + const alphasAndSpecials = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()" + const alphanumericsAndSpecials = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()" + rand.Seed(time.Now().UnixNano()) + pass := make([]byte, length) + + switch { + case numbers && specials: + for idx := 0; idx < length; idx++ { + pass[idx] = alphanumericsAndSpecials[rand.Int63()%int64(len(alphanumericsAndSpecials))] + } + case numbers: + for idx := 0; idx < length; idx++ { + pass[idx] = alphanumerics[rand.Int63()%int64(len(alphanumerics))] + } + case specials: + for idx := 0; idx < length; idx++ { + pass[idx] = alphasAndSpecials[rand.Int63()%int64(len(alphasAndSpecials))] + } + default: + for idx := 0; idx < length; idx++ { + pass[idx] = alphas[rand.Int63()%int64(len(alphas))] + } + } + + return string(pass) +} diff --git a/docker-compose.yml b/docker-compose.yml index fdbecc3..7fde9be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,16 @@ version: "3.7" services: redis: - image: "redis:alpine" - selfpass: + image: "redis:5.0.5" + volumes: + - "./data:/data" + server: build: . - entrypoint: - - server + restart: always ports: - "8080:8080" - secrets: - - ca_cert - - server_cert - - server_key -secrets: - ca_cert: - file: ./certs/ca.pem - server_cert: - file: ./certs/server.pem - server_key: - file: ./certs/server-key.pem + entrypoint: + - server + - -v + depends_on: + - redis diff --git a/gen_certs_go.sh b/gen_certs_go.sh new file mode 100755 index 0000000..64fe814 --- /dev/null +++ b/gen_certs_go.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh +ca=$(cat ./certs/ca.pem) +cert=$(cat ./certs/server.pem) +key=$(cat ./certs/server-key.pem) + +cat << EOM +// Code generated by inject-certs.sh, DO NOT EDIT. +package main + +const ca = \`${ca}\` +const cert = \`${cert}\` +const key = \`${key}\` +EOM diff --git a/go.mod b/go.mod index f2f84c4..abb33a5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ require ( github.com/Netflix/go-expect v0.0.0-20180928190340-9d1f4485533b // indirect github.com/atotto/clipboard v0.1.2 github.com/aws/aws-sdk-go-v2 v0.7.0 + github.com/boombuler/barcode v1.0.0 // indirect github.com/cloudflare/redoctober v0.0.0-20180928214028-3f826eedb692 + github.com/davecgh/go-spew v1.1.1 github.com/go-kit/kit v0.8.0 github.com/go-logfmt/logfmt v0.4.0 // indirect github.com/go-stack/stack v1.8.0 // indirect @@ -17,8 +19,8 @@ require ( github.com/mattn/go-isatty v0.0.7 // indirect github.com/mediocregopher/radix/v3 v3.2.3 github.com/mitchellh/go-homedir v1.1.0 + github.com/pquerna/otp v1.1.0 github.com/spf13/cobra v0.0.3 - github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.3.2 github.com/stretchr/testify v1.3.0 // indirect golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f // indirect diff --git a/go.sum b/go.sum index 96505bf..2737c74 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzb github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v0.7.0 h1:a5xRI/tBmUFKuAA0SOyEY2P1YhQb+jVOEI9P/7KfrP0= github.com/aws/aws-sdk-go-v2 v0.7.0/go.mod h1:17MaCZ9g0q5BIMxwzRQeiv8M3c8+W7iuBnlWAEprcxE= +github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/redoctober v0.0.0-20180928214028-3f826eedb692 h1:eVvf+nrm0mXV3JOh2c9vt+Pemh/TAUOjqRNrp1eyPmk= github.com/cloudflare/redoctober v0.0.0-20180928214028-3f826eedb692/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= @@ -81,6 +83,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.1.0 h1:q2gMsMuMl3JzneUaAX1MRGxLvOG6bzXV51hivBaStf0= +github.com/pquerna/otp v1.1.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= diff --git a/grpcurl.sh b/grpcurl.sh deleted file mode 100644 index e375792..0000000 --- a/grpcurl.sh +++ /dev/null @@ -1,6 +0,0 @@ -grpcurl -cacert ./certs/ca.pem \ - -cert ./certs/client.pem \ - -key ./certs/client-key.pem \ - -proto ./credentials/protobuf/service.proto \ - localhost:8080 \ - selfpass.credentials.CredentialService/GetAllMetadata