Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VALIDATE calls #13

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,33 @@ if err := oath.Put("test2", ykoath.HmacSha1, ykoath.Totp, 6, []byte("open sesame
}

```

## Authenticated example

If your Yubikey has a password, the above example will fail with an authentication required error. To authenticate, you will need to make a `Validate` call before any method that requires authentication. (Eg. `List()` or `Calculate()`). You can also determine if validation is required by checking the value of `Select.Challenge`.

Here is a partial example:
```
// Make select call
select, err = oath.Select()
if err != nil {
logger.Fatal(err)
}

// If required, authenticate with password
if select.Challenge != nil {
password := getYourUserPasswordFromSomewhere()
passKey := select.DeriveKey(string(bytePassword))
ok, err := oath.Validate(select, passKey)
if err != nil {
logger.Fatal(err)
}
if !ok {
logger.Fatal("failed validation, password is incorrect")
}
}

// Now you can call other functions
names, err := oath.List()

```
17 changes: 3 additions & 14 deletions algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,18 @@ package ykoath

import "fmt"

const (
// HmacSha1 describes a HMAC with SHA-1
HmacSha1 Algorithm = 0x01
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this - this is already a public constant. What good does the additional constant do?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I had done this initially to minimize the changes, but a single constant should be sufficient.


// HmacSha256 describes a HMAC with SHA-2 (256-bit)
HmacSha256 Algorithm = 0x02
yawn marked this conversation as resolved.
Show resolved Hide resolved

// HmacSha512 describes a HMAC with SHA-2 (512-bit)
HmacSha512 Algorithm = 0x03
yawn marked this conversation as resolved.
Show resolved Hide resolved
)

// Algorithm denotes the HMAc algorithm used for deriving the one-time passwords
type Algorithm byte

// String returns a string representation of the algorithm
func (a Algorithm) String() string {

switch a {
case HmacSha1:
case algoHMACSHA1:
return "HMAC-SHA1"
case HmacSha256:
case algoHMACSHA256:
return "HMAC-SHA256"
case HmacSha512:
case algoHMACSHA512:
return "HMAC-SHA512"
default:
return fmt.Sprintf("unknown %x", byte(a))
Expand Down
63 changes: 41 additions & 22 deletions calculate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,33 @@ package ykoath
import (
"encoding/binary"
"fmt"
"math"
"strings"
)

const (
errNoValuesFound = "no values found in response (% x)"
errUnknownName = "no such name configued (%s)"
errNoValuesFound = "no values found in response (% x)"
errUnknownName = "no such name configued (%s)"
errMultipleMatches = "multiple matches found (%s)"
touchRequired = "touch-required"
touchRequired = "touch-required"
)

// NoTouchCallback performas a noop for users that have no touch support
func NoTouchCallback(_ string) error {
return nil
}

// ErrorTouchCallback raises an error for users that have no touch support
func ErrorTouchCallback(_ string) error {
return fmt.Errorf("requires touch but no callback specified")
}

// Calculate is a high-level function that first identifies all TOTP credentials
// that are configured and returns the matching one (if no touch is required) or
// fires the callback and then fetches the name again while blocking during
// the device awaiting touch
func (o *OATH) Calculate(name string, touchRequiredCallback func(string) error) (string, error) {

res, err := o.calculateAll()
res, err := o.CalculateAll()

if err != nil {
return "", nil
Expand Down Expand Up @@ -50,17 +60,17 @@ func (o *OATH) Calculate(name string, touchRequiredCallback func(string) error)
return "", err
}

return o.calculate(key)
return o.CalculateOne(key)

}

return code, nil

}

// calculate implements the "CALCULATE" instruction to fetch a single
// CalculateOne implements the "CALCULATE" instruction to fetch a single
// truncated TOTP response
func (o *OATH) calculate(name string) (string, error) {
func (o *OATH) CalculateOne(name string) (string, error) {

var (
buf = make([]byte, 8)
Expand All @@ -69,9 +79,9 @@ func (o *OATH) calculate(name string) (string, error) {

binary.BigEndian.PutUint64(buf, uint64(timestamp))

res, err := o.send(0x00, 0xa2, 0x00, 0x01,
write(0x71, []byte(name)),
write(0x74, buf),
res, err := o.send(0x00, instCalculate, 0x00, rsTruncatedResponse,
write(tagName, []byte(name)),
write(tagChallenge, buf),
)

if err != nil {
Expand All @@ -82,7 +92,7 @@ func (o *OATH) calculate(name string) (string, error) {

switch tv.tag {

case 0x76:
case tagTruncatedResponse:
return otp(tv.value), nil

default:
Expand All @@ -95,9 +105,9 @@ func (o *OATH) calculate(name string) (string, error) {

}

// calculateAll implements the "CALCULATE ALL" instruction to fetch all TOTP
// CalculateAll implements the "CALCULATE ALL" instruction to fetch all TOTP
// tokens and their codes (or a constant indicating a touch requirement)
func (o *OATH) calculateAll() (map[string]string, error) {
func (o *OATH) CalculateAll() (map[string]string, error) {

var (
buf = make([]byte, 8)
Expand All @@ -108,8 +118,8 @@ func (o *OATH) calculateAll() (map[string]string, error) {

binary.BigEndian.PutUint64(buf, uint64(timestamp))

res, err := o.send(0x00, 0xa4, 0x00, 0x01,
write(0x74, buf),
res, err := o.send(0x00, instCalculateAll, 0x00, rsTruncatedResponse,
write(tagChallenge, buf),
)

if err != nil {
Expand All @@ -120,15 +130,23 @@ func (o *OATH) calculateAll() (map[string]string, error) {

switch tv.tag {

case 0x71:
case tagName:
names = append(names, string(tv.value))

case 0x7c:
case tagTouch:
codes = append(codes, touchRequired)

case 0x76:
case tagResponse:
o.Debug("tag no full response: %x", tv.value)
return nil, fmt.Errorf("unable to handle full response %x", tv.value)

case tagTruncatedResponse:
codes = append(codes, otp(tv.value))

case tagNoResponse:
o.Debug("tag no response. Is HOTP %x", tv.value)
return nil, fmt.Errorf("unable to handle HOTP response %x", tv.value)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors messages should generally be defined as private constants on the top of the file, prefixed with err. The actual error should be wrapped using the errors package like so: errors.Wrapf(err, const-message, parameters...)

This patterns makes it easy to see what planned errors could be yielded by package.

Regarding the 0x77 - under which circumstances does this appear?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. I think I understood what you meant here, but now I'm a little confused.

It looks like all the error strings you used are at the top of ykoath.go, but only as strings. So one can't use errors.Is to determine if they are a particular error.

Would it make sense to have those constants actually be errors? Eg. const errFailedToListSuitableReader = errors.New("failed to connect to reader").

This, however doesn't allow formatting as is done with errUnknownTag. The go docs seem to suggest creating a struct that contains a field for the tag and then defining an Error() string function on that struct that returns the formatted text, however that's a bit convoluted. The other thing I can think of is defining const errUnknownTag = errors.New("unknown tag") and then catching and returning fmt.Errorf("%w (%s)", errUnknownTag, tag) so that one handling the error could still tell that it wraps errUnknownTag.

I'm really a go hobbyist, so I'm not as familiar with the current best practices for errors in go as they seem to change every couple of months. 😄


default:
return nil, fmt.Errorf(errUnknownTag, tv.tag)
}
Expand All @@ -147,9 +165,10 @@ func (o *OATH) calculateAll() (map[string]string, error) {

// otp converts a value into a (6 or 8 digits) one-time password
func otp(value []byte) string {

digits := value[0]
digits := int(value[0])
code := binary.BigEndian.Uint32(value[1:])
// Limit code to a maximum number of digits
code = code % uint32(math.Pow(10.0, float64(digits)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Und which circumstances would the code be > 8 digits?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some TOTP that were supposed to be 6 digits (this was the value of digits), and they were returned as 8 digits. This will trim them to length of whatever the the value of digits is. This logic is the same as in yubikey-manager

// Format as string with a minimum number of digits, padded with 0s
return fmt.Sprintf(fmt.Sprintf("%%0%dd", digits), code)

}
9 changes: 6 additions & 3 deletions code.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ type code []byte
// Error return the encapsulated error string
func (c code) Error() string {

if bytes.Equal(c, []byte{0x6a, 0x80}) {
if bytes.Equal(c, rsErrWrongSyntax) {
return "wrong syntax"
} else if bytes.Equal(c, []byte{0x69, 0x84}) {
} else if bytes.Equal(c, rsErrRequiresAuth) {
return "requires auth"
} else if bytes.Equal(c, rsErrNoSuchObject) {
return "no such object"
} else if bytes.Equal(c, rsErrGenericError) {
return "generic error"
}

return fmt.Sprintf("unknown (% x)", []byte(c))

}

// IsMore indicates more data that needs to be fetched
Expand Down
61 changes: 61 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ykoath
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer the constants to be private tbh - they do help readability but trigger linting advice when undocumented and public.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I don't believe the API that this package exposes requires someone to access these either. I'll rename them.


// Constants found in here come from the YKOATH documentation.
// https://developers.yubico.com/OATH/YKOATH_Protocol.html

const (
// Instructions
instPut = 0x01 // Requires auth
instDelete = 0x02 // Requires auth
instSetCode = 0x03 // Requires auth
instReset = 0x04
instList = 0xa1 // Requires auth
instCalculate = 0xa2 // Requires auth
instValidate = 0xa3
instCalculateAll = 0xa4 // Requires auth
instSendRemaining = 0xa5 // Requires auth

// Response size
rsFullResponse = 0x00
rsTruncatedResponse = 0x01

// Algorithms
algoHMACSHA1 = 0x01
algoHMACSHA256 = 0x02
algoHMACSHA512 = 0x03

// OATH Types
typeHOTP = 0x10
typeTOTP = 0x20

// Properties
propIncreasing = 0x01
propRequireTouch = 0x02

// Tags
tagBlank = 0x00
tagName = 0x71
tagNameList = 0x72
tagKey = 0x73
tagChallenge = 0x74
tagResponse = 0x75
tagTruncatedResponse = 0x76
tagNoResponse = 0x77
tagProperty = 0x78
tagVersion = 0x79
tagImf = 0x7a
tagAlgorithm = 0x7b
tagTouch = 0x7c

// Mask
maskAlgo = 0x0f
maskType = 0xf0
)

var (
// Responses
rsErrWrongSyntax = []byte{0x6a, 0x80}
rsErrRequiresAuth = []byte{0x69, 0x82}
rsErrNoSuchObject = []byte{0x69, 0x84}
rsErrGenericError = []byte{0x65, 0x81}
)
4 changes: 2 additions & 2 deletions delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package ykoath
// Delete sends a "DELETE" instruction, removing one named OATH credential
func (o *OATH) Delete(name string) error {

_, err := o.send(0x00, 0x02, 0x00, 0x00,
write(0x71, []byte(name)))
_, err := o.send(0x00, instDelete, 0x00, 0x00,
write(tagName, []byte(name)))

return err

Expand Down
2 changes: 1 addition & 1 deletion examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func Example() {
_, _ = oath.Select()

// add the testvector
_ = oath.Put("testvector", HmacSha1, Totp, 8, []byte("12345678901234567890"), false)
_ = oath.Put("testvector", algoHMACSHA1, Totp, 8, []byte("12345678901234567890"), false)

names, _ := oath.List()

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
module github.com/yawn/ykoath

go 1.15

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95
github.com/pkg/errors v0.8.1
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
8 changes: 4 additions & 4 deletions list.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (o *OATH) List() ([]*Name, error) {

var names []*Name

res, err := o.send(0x00, 0xa1, 0x00, 0x00)
res, err := o.send(0x00, instList, 0x00, 0x00)

if err != nil {
return nil, err
Expand All @@ -30,12 +30,12 @@ func (o *OATH) List() ([]*Name, error) {
for _, tv := range res {

switch tv.tag {
case 0x72:
case tagNameList:

name := &Name{
Algorithm: Algorithm(tv.value[0] & 0x0f),
Algorithm: Algorithm(tv.value[0] & maskAlgo),
Name: string(tv.value[1:]),
Type: Type(tv.value[0] & 0xf0),
Type: Type(tv.value[0] & maskType),
}

names = append(names, name)
Expand Down
11 changes: 6 additions & 5 deletions put.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@ func (o *OATH) Put(name string, a Algorithm, t Type, digits uint8, key []byte, t
}

var (
alg = (0xf0|byte(a))&0x0f | byte(t)
// High 4 bits is type, low 4 bits is algorithm
alg = (maskType|byte(a))&maskAlgo | byte(t)
dig = byte(digits)
prp []byte
)

if touch {
prp = write(0x78, []byte{0x02})
prp = write(tagProperty, []byte{propRequireTouch})
}

_, err := o.send(0x00, 0x01, 0x00, 0x00,
write(0x71, []byte(name)),
write(0x73, []byte{alg, dig}, key),
_, err := o.send(0x00, instPut, 0x00, 0x00,
write(tagName, []byte(name)),
write(tagKey, []byte{alg, dig}, key),
prp,
)

Expand Down
Loading