package service import ( "bufio" "context" "encoding/json" "fmt" "os" "strings" "github.com/keys-pub/keys" "github.com/keys-pub/keys-ext/auth/fido2" "github.com/pkg/errors" "github.com/urfave/cli" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func authCommands(client *Client) []cli.Command { return []cli.Command{ cli.Command{ Name: "auth", Usage: "Authorize", Flags: []cli.Flag{ cli.StringFlag{Name: "password, pin", Usage: "password or pin"}, cli.BoolFlag{Name: "token", Usage: "output token only"}, cli.StringFlag{Name: "type, t", Usage: "auth type: password, fido2-hmac-secret", Value: "password"}, cli.StringFlag{Name: "client", Value: "cli", Hidden: true}, cli.StringFlag{Name: "device", Value: "device path or product name (for FIDO2)"}, }, Aliases: []string{"unlock"}, Subcommands: []cli.Command{ authProvisionCommand(client), authProvisionsCommand(client), authDeprovisionCommand(client), authVaultCommand(client), changePasswordCommand(client), authDevicesCommand(client), authResetCommand(client), }, Action: func(c *cli.Context) error { if !c.GlobalBool("test") { if err := checkForAppConflict(); err != nil { logger.Warningf("%s", err) } } status, err := client.RPCClient().RuntimeStatus(context.TODO(), &RuntimeStatusRequest{}) if err != nil { return err } setupNeeded := status.AuthStatus == AuthSetupNeeded logger.Infof("Auth setup needed: %t", setupNeeded) clientName := c.String("client") if clientName == "" { return errors.Errorf("no client name") } authType, err := chooseAuth("How do you want to authorize?", c.String("type")) if err != nil { return err } var authToken string var authErr error if setupNeeded { logger.Infof("Auth setup...") switch authType { case PasswordAuth: authToken, authErr = passwordAuthSetup(context.TODO(), client, clientName, c.String("password")) case FIDO2HMACSecretAuth: authToken, authErr = fido2AuthSetup(context.TODO(), client, clientName, c.String("device"), c.String("pin")) } } else { logger.Infof("Auth unlock...") switch authType { case PasswordAuth: authToken, authErr = passwordAuthUnlock(context.TODO(), client, clientName, c.String("password")) case FIDO2HMACSecretAuth: authToken, authErr = fido2AuthUnlock(context.TODO(), client, clientName, c.String("pin")) } } if authErr != nil { return authErr } if c.Bool("token") { fmt.Println(authToken) return nil } fmt.Printf("export KEYS_AUTH=\"%s\"\n", authToken) fmt.Printf("# For shell:\n") fmt.Printf("# export KEYS_AUTH=`keys auth -token`\n") fmt.Printf("#\n") fmt.Printf("# or using eval:\n") fmt.Printf("# eval $(keys auth)\n") fmt.Printf("#\n") fmt.Printf("# For Powershell:\n") fmt.Printf("# $env:KEYS_AUTH = (keys auth -token)\n") return nil }, }, cli.Command{ Name: "lock", Usage: "Lock", Flags: []cli.Flag{}, Action: func(c *cli.Context) error { _, err := client.RPCClient().AuthLock(context.TODO(), &AuthLockRequest{}) if err != nil { return err } return nil }, }, } } func authVaultCommand(client *Client) cli.Command { return cli.Command{ Name: "vault", Usage: "Connect to vault", Flags: []cli.Flag{ cli.StringFlag{Name: "phrase", Usage: "Phrase from vault auth"}, }, Action: func(c *cli.Context) error { reader := bufio.NewReader(os.Stdin) fmt.Fprintf(os.Stderr, "Vault phrase: ") phrase, err := reader.ReadString('\n') if err != nil { return err } if _, err := client.RPCClient().AuthVault(context.TODO(), &AuthVaultRequest{ Phrase: phrase, }); err != nil { return err } return nil }, } } func changePasswordCommand(client *Client) cli.Command { return cli.Command{ Name: "change-password", Usage: "Change password", Flags: []cli.Flag{}, Action: func(c *cli.Context) error { old, err := readPassword("Old password:", false) if err != nil { return err } new, err := readVerifyPassword("New password:") if err != nil { return err } if _, err := client.RPCClient().AuthPasswordChange(context.TODO(), &AuthPasswordChangeRequest{ Old: old, New: new, }); err != nil { return err } return nil }, } } func authProvisionCommand(client *Client) cli.Command { return cli.Command{ Name: "provision", Usage: "Provision", Flags: []cli.Flag{ cli.StringFlag{Name: "password, pin, p", Usage: "password or pin"}, cli.StringFlag{Name: "type", Usage: "auth type: password, fido2-hmac-secret"}, cli.StringFlag{Name: "device", Value: "", Usage: "device path or product name"}, }, Action: func(c *cli.Context) error { rts, err := client.RPCClient().RuntimeStatus(context.TODO(), &RuntimeStatusRequest{}) if err != nil { return err } switch rts.AuthStatus { case AuthSetupNeeded: return status.Error(codes.Unauthenticated, "auth setup needed") case AuthLocked: return status.Error(codes.Unauthenticated, "auth locked") } authType, err := chooseAuth("How do you want to provision?", c.String("type")) if err != nil { return err } logger.Infof("Auth provision...") switch authType { case PasswordAuth: if err := passwordAuthProvision(context.TODO(), client, c.String("password")); err != nil { return err } case FIDO2HMACSecretAuth: if err := fido2AuthProvision(context.TODO(), client, c.String("device"), c.String("pin")); err != nil { return err } } return nil }, } } func authDevicesCommand(client *Client) cli.Command { return cli.Command{ Name: "devices", Usage: "Devices", Flags: []cli.Flag{}, Action: func(c *cli.Context) error { resp, err := client.FIDO2Client().Devices(context.TODO(), &fido2.DevicesRequest{}) if err != nil { return err } for _, device := range resp.Devices { typeResp, err := client.FIDO2Client().DeviceType(context.TODO(), &fido2.DeviceTypeRequest{Device: device.Path}) if err != nil { return err } if typeResp.Type != fido2.FIDO2Device { continue } infoResp, err := client.FIDO2Client().DeviceInfo(context.TODO(), &fido2.DeviceInfoRequest{Device: device.Path}) if err != nil { return err } out := struct { Device *fido2.Device `json:"device"` Info *fido2.DeviceInfo `json:"info"` }{ Device: device, Info: infoResp.Info, } b, err := json.Marshal(out) if err != nil { return err } fmt.Println(string(b)) } return nil }, } } func authResetCommand(client *Client) cli.Command { return cli.Command{ Name: "reset", Usage: "Reset", Flags: []cli.Flag{ cli.BoolFlag{Name: "force", Usage: "force"}, cli.StringFlag{Hidden: true, Name: "app", Value: "Keys"}, }, Action: func(c *cli.Context) error { if !c.Bool("force") { reader := bufio.NewReader(os.Stdin) words := keys.RandWords(6) fmt.Printf("Are you sure you want to reset auth and remove your vault?\n") fmt.Printf("If so enter this phrase: %s\n\n", words) text, _ := reader.ReadString('\n') text = strings.Trim(text, "\r\n") fmt.Println("") if text != words { fmt.Println("Phrase doesn't match.") os.Exit(1) } } _, err := client.RPCClient().AuthReset(context.TODO(), &AuthResetRequest{ AppName: c.String("app"), }) if err != nil { return err } fmt.Println("Auth reset.") return nil }, } } func chooseAuth(title string, arg string) (AuthType, error) { if arg != "" { return authTypeFromString(arg) } for { reader := bufio.NewReader(os.Stdin) fmt.Fprintln(os.Stderr, title) fmt.Fprintln(os.Stderr, "(p) Password") fmt.Fprintln(os.Stderr, "(f) FIDO2 hmac-secret") input, err := reader.ReadString('\n') if err != nil { return UnknownAuth, err } authType, err := authTypeFromString(input) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) } else { return authType, nil } } } func authTypeFromString(s string) (AuthType, error) { switch strings.TrimSpace(strings.ToLower(s)) { case "p", "password": return PasswordAuth, nil case "f", "fido2-hmac-secret": return FIDO2HMACSecretAuth, nil default: return UnknownAuth, errors.Errorf("unknown auth type: %s", s) } } func authProvisionsCommand(client *Client) cli.Command { return cli.Command{ Name: "provisions", Usage: "Provisions", Flags: []cli.Flag{}, Action: func(c *cli.Context) error { ctx := context.TODO() resp, err := client.RPCClient().AuthProvisions(ctx, &AuthProvisionsRequest{}) if err != nil { return err } printMessage(resp) return nil }, } } func authDeprovisionCommand(client *Client) cli.Command { return cli.Command{ Name: "deprovision", Usage: "Deprovision", Flags: []cli.Flag{}, Action: func(c *cli.Context) error { id := c.Args().First() if id == "" { return errors.Errorf("specify a provision id") } _, err := client.RPCClient().AuthDeprovision(context.TODO(), &AuthDeprovisionRequest{ ID: id, }) if err != nil { return err } return nil }, } }