package main import ( "errors" "fmt" "github.com/spf13/cobra" "git.wxccs.org/iceking2nd/winauth-go/internal/authenticator" "git.wxccs.org/iceking2nd/winauth-go/internal/config" "git.wxccs.org/iceking2nd/winauth-go/internal/i18n" ) func newGetCmd() *cobra.Command { var noNewline, advance bool cmd := &cobra.Command{ Use: "get NAME", Short: "Print the current OTP for an entry", Long: `Print the current one-time password for the entry named NAME. The name must match exactly (case-sensitive). For HOTP (counter-based) entries, this command refuses by default because generating a code advances the counter. Use --advance to confirm that you want to consume a counter value; the config file will be updated automatically.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return runGet(cmd, args[0], noNewline, advance) }, } cmd.Flags().BoolVarP(&noNewline, "no-newline", "n", false, "Do not append a newline after the code") cmd.Flags().BoolVar(&advance, "advance", false, "Advance HOTP counter (required for counter-based entries)") return cmd } func runGet(cmd *cobra.Command, name string, noNewline, advance bool) error { path := resolveConfigPath(cmd) lang := preferredLanguage(path) _ = i18n.Init(lang) cfg, passphrase, err := cliLoadConfig(path) if err != nil { if errors.Is(err, config.ErrPasswordWrong) { return fmt.Errorf("wrong password") } return fmt.Errorf("load config: %w", err) } // Find entry by exact name. var target *config.Entry var matches []string for i := range cfg.Entries { if cfg.Entries[i].Name == name { target = &cfg.Entries[i] break } matches = append(matches, cfg.Entries[i].Name) } if target == nil { return fmt.Errorf("entry %q not found", name) } a, err := buildAuth(*target) if err != nil { return fmt.Errorf("build authenticator: %w", err) } // HOTP safety gate. if a.Name() == "hotp" && !advance { return fmt.Errorf("entry %q is counter-based (HOTP); re-run with --advance to consume a counter value", name) } code, err := a.CurrentCode() if err != nil { return fmt.Errorf("compute OTP: %w", err) } if noNewline { fmt.Print(code) } else { fmt.Println(code) } // Persist the updated counter for HOTP entries. if a.Name() == "hotp" { target.SecretRaw = a.SecretData() if err := config.SaveYAML(cfg, path, passphrase); err != nil { return fmt.Errorf("save config after HOTP advance: %w", err) } } return nil } // buildAuth mirrors the switch in internal/ui/convert.go. Duplicated here // so the CLI binary does not depend on the UI package. func buildAuth(e config.Entry) (authenticator.Authenticator, error) { var a authenticator.Authenticator switch e.Vendor { case "google", "": a = authenticator.NewGoogleAuthenticator() case "microsoft": a = authenticator.NewMicrosoftAuthenticator() case "okta": a = authenticator.NewOktaVerifyAuthenticator() case "hotp": a = authenticator.NewHOTPAuthenticator() case "battlenet": a = authenticator.NewBattleNetAuthenticator() case "steam": a = authenticator.NewSteamAuthenticator() default: return nil, fmt.Errorf("unknown vendor %q", e.Vendor) } if err := a.SetSecretData(e.SecretRaw); err != nil { return nil, fmt.Errorf("decode entry %q: %w", e.Name, err) } return a, nil }