727be3557a
- winauth list: tabwriter 列出所有条目(名称/vendor/icon/hotkey) - winauth get NAME: 输出当前 OTP,-n 不换行,--advance 允许 HOTP - 加密配置自动 tty 隐藏密码提示(golang.org/x/term) - --config 改为 PersistentFlag,子命令可继承 - HOTP get 默认拒绝,需 --advance 显式确认并保存配置 - CLI 不运行 GUI、不持有单实例锁
121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
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
|
|
}
|