-
Notifications
You must be signed in to change notification settings - Fork 1
/
oath.go
137 lines (114 loc) · 4.15 KB
/
oath.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package ykmangoath
import (
"context"
"fmt"
"regexp"
"strings"
"golang.org/x/exp/slices"
)
// deviceSerialPattern ensures the given Yubikey device serial is either empty string or at least 8 digits
var deviceSerialPattern = regexp.MustCompile(`^$|^\d+$`)
// OathAccounts represents a the main functionality of Yubikey OATH accounts.
type OathAccounts struct {
passwordPrompt func(ctx context.Context) (string, error)
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, serial: serial}
oa.ykman = *NewYkman(ctx, oa.serial)
result := deviceSerialPattern.FindString(oa.serial)
if result == "" {
return oa, fmt.Errorf("%w: %v", ErrDeviceSerial, oa.serial)
}
oa.serial = serial
return oa, nil
}
// GetSerial returns the currently configured Yubikey device serial.
func (oa *OathAccounts) GetSerial() string {
return oa.serial
}
// IsAvailable checks whether the Yubikey device is connected & available
func (oa *OathAccounts) IsAvailable() bool {
_, err := oa.ykman.Execute([]string{"info"})
return err == nil
}
// IsPasswordProtected checks whether the OATH application is password protected
func (oa *OathAccounts) IsPasswordProtected() bool {
_, err := oa.ykman.Execute([]string{"oath", "accounts", "list"})
return err == ErrOathAccountPasswordProtected
}
// SetPassword directly configures the Yubikey OATH application password.
// Mutually exclusive with SetPasswordPrompt.
func (oa *OathAccounts) SetPassword(password string) error {
if oa.passwordPrompt != nil {
return fmt.Errorf("%w: password prompt already set", ErrPasswordSetup)
}
oa.password = password
return nil
}
// SetPasswordPrompt configures a function that will be called upon if/when
// the Yubikey OATH application password is required.
// Mutually exclusive with SetPassword.
func (oa *OathAccounts) SetPasswordPrompt(prompt func(ctx context.Context) (string, error)) error {
if oa.password != "" {
return fmt.Errorf("%w: password already set", ErrPromptSetup)
}
oa.passwordPrompt = prompt
return nil
}
// GetPassword returns the password that successfully unlocked the Yubikey OATH application.
func (oa *OathAccounts) GetPassword() (string, error) {
if oa.ykman.password == "" {
return "", ErrNoPassword
}
return oa.ykman.password, nil
}
// List returns a list of configured accounts in the Yubikey OATH application.
func (oa *OathAccounts) List() ([]string, error) {
oa.ensurePrompt()
output, err := oa.ykman.ExecuteWithPrompt([]string{"oath", "accounts", "list"}, oa.passwordPrompt)
if err != nil {
return nil, err
}
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) {
oa.ensurePrompt()
output, err := oa.ykman.ExecuteWithPrompt([]string{"oath", "accounts", "code", "--single", account}, oa.passwordPrompt)
if err != nil {
return output, err
}
return parseCode(output)
}
// ensurePrompt checks if prompt is not configured by user and then assigns
// a simple function as prompt that returns oa.password.
func (oa *OathAccounts) ensurePrompt() {
if oa.passwordPrompt == nil {
oa.passwordPrompt = func(ctx context.Context) (string, error) { return oa.password, nil }
}
}
// parseCode retrieves the generated 6 digit OATH TOTP code from output
func parseCode(output string) (string, error) {
result := yubikeyTokenFindPattern.FindString(strings.TrimSpace(output))
if result == "" {
return "", ErrOathAccountCodeParseFailed
}
return result, nil
}
// yubikeyTokenFindPattern describes the regexp that will match OATH TOTP MFA token code from Yubikey
var yubikeyTokenFindPattern = regexp.MustCompile(`\d{6}\d*$`)