yk-cli/main.go

143 lines
3.3 KiB
Go
Raw Normal View History

2020-12-11 04:55:55 +00:00
package main
import (
"errors"
"flag"
"fmt"
"os"
"syscall"
"git.iamthefij.com/iamthefij/slog"
"github.com/yawn/ykoath"
"github.com/zalando/go-keyring"
2020-12-11 04:55:55 +00:00
"golang.org/x/term"
)
var (
oath *ykoath.OATH
serviceName = "com.iamthefij.yk-cli"
version = "dev"
errFailedValidation = errors.New("failed validation, password may be incorrect")
)
func setPassword(s *ykoath.Select) error {
fmt.Print("Enter Password: ")
bytePassword, err := term.ReadPassword(syscall.Stdin)
if err != nil {
2021-01-19 01:29:20 +00:00
return fmt.Errorf("failed reading password from input: %w", err)
2020-12-11 04:55:55 +00:00
}
2021-01-19 01:29:20 +00:00
password := string(bytePassword)
// get key
key := s.DeriveKey(password)
2020-12-11 04:55:55 +00:00
2021-01-19 01:29:20 +00:00
// verify password is correct with a validate call
2020-12-11 04:55:55 +00:00
ok, err := oath.Validate(s, key)
if err != nil {
2021-01-19 01:29:20 +00:00
return fmt.Errorf("error in validate: %w", err)
2020-12-11 04:55:55 +00:00
}
if !ok {
return errFailedValidation
}
2021-01-19 01:29:20 +00:00
err = keyring.Set(
2020-12-11 04:55:55 +00:00
serviceName,
s.DeviceID(),
2021-01-19 01:29:20 +00:00
password,
2020-12-11 04:55:55 +00:00
)
2021-01-19 01:29:20 +00:00
if err != nil {
return fmt.Errorf("error saving password in keyring: %w", err)
}
return nil
2020-12-11 04:55:55 +00:00
}
2021-01-19 01:29:20 +00:00
func getPasskey(s *ykoath.Select) ([]byte, error) {
password, err := keyring.Get(serviceName, s.DeviceID())
if err != nil {
return nil, fmt.Errorf("error retrieving key from keyring: %w", err)
}
return s.DeriveKey(password), nil
2020-12-11 04:55:55 +00:00
}
2021-07-26 05:14:45 +00:00
func usage() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage:\n %s [target site]\n\n", os.Args[0])
fmt.Fprintf(flag.CommandLine.Output(), "Prints TOTP code for provided target site. If no site is provided, %s will list all sites.\n\n", os.Args[0])
fmt.Fprint(flag.CommandLine.Output(), "If a touch is required for your code, the command will not return until the your key is touched.\n\n")
flag.PrintDefaults()
}
2020-12-11 04:55:55 +00:00
func main() {
2021-07-26 05:14:45 +00:00
flag.Usage = usage
2020-12-11 04:55:55 +00:00
flag.BoolVar(&slog.DebugLevel, "debug", false, "enable debug logging")
showVersion := flag.Bool("version", false, "print version and exit")
2021-07-26 05:14:45 +00:00
shouldSetPassword := flag.Bool("set-password", false, "prompt for key password and store in system keychain")
2020-12-11 04:55:55 +00:00
flag.Parse()
if *showVersion {
fmt.Printf("Version %s\n", version)
os.Exit(0)
}
var err error
oath, err = ykoath.New()
slog.FatalOnErr(err, "failed to initialize new oath")
defer oath.Close()
if slog.DebugLevel {
oath.Debug = slog.Debug
}
// Select oath to begin
s, err := oath.Select()
slog.FatalOnErr(err, "failed to select oath")
// Check to see if we are trying to set a password
2021-07-26 05:14:45 +00:00
if *shouldSetPassword {
2020-12-11 04:55:55 +00:00
err = setPassword(s)
slog.FatalOnErr(err, "failed to save password")
return
}
// If required, authenticate with password from keychain
if s.Challenge != nil {
2021-01-19 01:29:20 +00:00
passKey, err := getPasskey(s)
2020-12-11 04:55:55 +00:00
slog.FatalOnErr(err, "failed retrieving password key")
2021-01-19 01:29:20 +00:00
ok, err := oath.Validate(s, passKey)
2020-12-11 04:55:55 +00:00
slog.FatalOnErr(err, "validation failed")
if !ok {
slog.Fatal("failed validation, password is incorrect")
}
2021-01-19 01:29:20 +00:00
} else {
slog.Debug("no challenge required")
2020-12-11 04:55:55 +00:00
}
2021-07-26 05:14:45 +00:00
if flag.NArg() == 0 || flag.Arg(0) == "list" {
2020-12-11 04:55:55 +00:00
// List names only
names, err := oath.List()
slog.FatalOnErr(err, "failed to list names")
for _, name := range names {
fmt.Println(name.Name)
}
} else {
name := flag.Arg(0)
code, err := oath.Calculate(name, func(string) error {
fmt.Fprintf(os.Stderr, "Touch key to generate code for %s...", name)
return nil
})
2020-12-11 04:55:55 +00:00
slog.FatalOnErr(err, "failed to retrieve credential")
fmt.Println(code)
}
}