Skip to content

Commit

Permalink
feat: hasAccount & fix password return
Browse files Browse the repository at this point in the history
  • Loading branch information
aripalo committed Apr 17, 2022
1 parent 06826a7 commit 6dd04f5
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 63 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.0 // indirect
github.com/stretchr/testify v1.7.1 // indirect
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd h1:zVFyTKZN/Q7mNRWSs1GOYnHM9NiFSJ54YVRsD0rNWT4=
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
55 changes: 23 additions & 32 deletions oath.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"regexp"
"strings"

"golang.org/x/exp/slices"
)

// deviceSerialPattern ensures the given Yubikey device serial is either empty string or at least 8 digits
Expand All @@ -16,14 +18,16 @@ type OathAccounts struct {
ctx context.Context
serial string
password string
ykman Ykman
}

// New defines a new instance of OathAccounts.
func New(ctx context.Context, serial string) (OathAccounts, error) {
oa := OathAccounts{ctx: ctx}
result := deviceSerialPattern.FindString(serial)
oa.ykman = *NewYkman(ctx, oa.serial)
result := deviceSerialPattern.FindString(oa.serial)
if result == "" {
return oa, fmt.Errorf("%w: %v", ErrDeviceSerial, serial)
return oa, fmt.Errorf("%w: %v", ErrDeviceSerial, oa.serial)
}
oa.serial = serial
return oa, nil
Expand All @@ -36,25 +40,13 @@ func (oa *OathAccounts) GetSerial() string {

// IsAvailable checks whether the Yubikey device is connected & available
func (oa *OathAccounts) IsAvailable() bool {
queryOptions := ykmanOptions{
serial: oa.serial,
password: "",
args: []string{"info"},
}

_, err := executeYkman(oa.ctx, queryOptions)
_, err := oa.ykman.Execute([]string{"info"})
return err == nil
}

// IsPasswordProtected checks whether the OATH application is password protected
func (oa *OathAccounts) IsPasswordProtected() bool {
queryOptions := ykmanOptions{
serial: oa.serial,
password: "",
args: []string{"oath", "accounts", "list"},
}

_, err := executeYkman(oa.ctx, queryOptions)
_, err := oa.ykman.Execute([]string{"oath", "accounts", "list"})
return err == ErrOathAccountPasswordProtected
}

Expand All @@ -81,22 +73,16 @@ func (oa *OathAccounts) SetPasswordPrompt(prompt func(ctx context.Context) (stri

// GetPassword returns the password that successfully unlocked the Yubikey OATH application.
func (oa *OathAccounts) GetPassword() (string, error) {
if oa.password == "" {
if oa.ykman.password == "" {
return "", ErrNoPassword
}
return oa.password, nil
return oa.ykman.password, nil
}

// List returns a list of configured accounts in the Yubikey OATH application.
func (oa *OathAccounts) List() ([]string, error) {
queryOptions := ykmanOptions{
serial: oa.serial,
password: oa.password,
args: []string{"oath", "accounts", "list"},
}

oa.ensurePrompt()
output, err := executeYkmanWithPrompt(oa.ctx, queryOptions, oa.passwordPrompt)
output, err := oa.ykman.ExecuteWithPrompt([]string{"oath", "accounts", "list"}, oa.passwordPrompt)

if err != nil {
return nil, err
Expand All @@ -105,17 +91,22 @@ func (oa *OathAccounts) List() ([]string, error) {
return getLines(output), nil
}

// HasAccount returns a boolean indicating if the device has the given account
// configured in its OATH application.
func (oa *OathAccounts) HasAccount(account string) (bool, error) {
accounts, err := oa.List()
if err != nil {
return false, err
}

return slices.Contains(accounts, account), err
}

// Code requests a Time-based one-time password (TOTP) 6-digit code for given
// account (such as "<issuer>:<name>") from Yubikey OATH application.
func (oa *OathAccounts) Code(account string) (string, error) {
queryOptions := ykmanOptions{
serial: oa.serial,
password: oa.password,
args: []string{"oath", "accounts", "code", "--single", account},
}

oa.ensurePrompt()
output, err := executeYkmanWithPrompt(oa.ctx, queryOptions, oa.passwordPrompt)
output, err := oa.ykman.ExecuteWithPrompt([]string{"oath", "accounts", "code", "--single", account}, oa.passwordPrompt)

if err != nil {
return output, err
Expand Down
53 changes: 22 additions & 31 deletions ykman.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,30 @@ import (
"syscall"
)

// ykmanOptions controls the ykman operation performed
type ykmanOptions struct {
type Ykman struct {
ctx context.Context
serial string
password string
args []string
}

func executeYkmanWithPrompt(ctx context.Context, options ykmanOptions, prompt func(ctx context.Context) (string, error)) (string, error) {
result, err := executeYkman(ctx, options)
if err != ErrOathAccountPasswordProtected {
return result, err
}

password, err := prompt(ctx)
if err != nil {
return "", err
}

options.password = password

return executeYkman(ctx, options)
func NewYkman(ctx context.Context, serial string) *Ykman {
return &Ykman{ctx: ctx, serial: serial}
}

// executeYkman executes ykman with given options and handles most common errors
func executeYkman(ctx context.Context, options ykmanOptions) (string, error) {

args := defineYkmanArgs(options)
func (y *Ykman) Execute(args []string) (string, error) {
// only apply device argument if an id is given
if y.serial != "" {
args = append(args, "--device", y.serial)
}

// define the ykman command to be run
cmd := exec.CommandContext(ctx, "ykman", args...)
cmd := exec.CommandContext(y.ctx, "ykman", args...)

// in case a password is provided, provide it to ykman via stdin
// it's better to pass it in via stdin as it will fail on empty string immediately
// if the oath is password protected
var b bytes.Buffer
b.Write([]byte(fmt.Sprintf("%s\n", options.password)))
b.Write([]byte(fmt.Sprintf("%s\n", y.password)))
cmd.Stdin = &b

// redirect stdout & stderr into byte buffer
Expand All @@ -55,23 +43,26 @@ func executeYkman(ctx context.Context, options ykmanOptions) (string, error) {
// execute the ykman command
err := cmd.Run()

err = processYkmanErrors(err, errb.String(), options.password)
err = processYkmanErrors(err, errb.String(), y.password)

// finally return the ykman output
return outb.String(), err
}

func defineYkmanArgs(options ykmanOptions) []string {
args := []string{}
func (y *Ykman) ExecuteWithPrompt(args []string, prompt func(ctx context.Context) (string, error)) (string, error) {
result, err := y.Execute(args)
if err != ErrOathAccountPasswordProtected {
return result, err
}

// only apply device argument if an id is given
if options.serial != "" {
args = append(args, "--device", options.serial)
password, err := prompt(y.ctx)
if err != nil {
return "", err
}

args = append(args, options.args...)
y.password = password

return args
return y.Execute(args)
}

func processYkmanErrors(err error, outputStderr string, password string) error {
Expand Down
3 changes: 3 additions & 0 deletions ykman_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)

/*
func TestDefineYkmanArgs(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -59,6 +60,8 @@ func TestDefineYkmanArgs(t *testing.T) {
}
}
*/

func TestProcessYkmanErrors(t *testing.T) {

genericErr := errors.New("just for testing")
Expand Down

0 comments on commit 6dd04f5

Please sign in to comment.