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 4 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
6 changes: 3 additions & 3 deletions algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ 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.

HmacSha1 Algorithm = A_HMAC_SHA1

// HmacSha256 describes a HMAC with SHA-2 (256-bit)
HmacSha256 Algorithm = 0x02
HmacSha256 Algorithm = A_HMAC_SHA256

// HmacSha512 describes a HMAC with SHA-2 (512-bit)
HmacSha512 Algorithm = 0x03
HmacSha512 Algorithm = A_HMAC_SHA512
)

// Algorithm denotes the HMAc algorithm used for deriving the one-time passwords
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, INST_CALCULATE, 0x00, RS_TRUNCATED_RESPONSE,
write(TAG_NAME, []byte(name)),
write(TAG_CHALLENGE, buf),
)

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

switch tv.tag {

case 0x76:
case TAG_TRUNCATED_RESPONSE:
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, INST_CALCULATE_ALL, 0x00, RS_TRUNCATED_RESPONSE,
write(TAG_CHALLENGE, buf),
)

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

switch tv.tag {

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

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

case 0x76:
case TAG_RESPONSE:
Copy link
Owner

Choose a reason for hiding this comment

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

Can you elaborate the scenario in which we need to handle a 0x75?

Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure if there is a legitimate case for this, however it is included as a possible response in the spec. The result (once properly wrapping errors, as suggested) will be the same as not handling it, except with an error message that will be somewhat more helpful.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok, understood.

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

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

case TAG_NO_RESPONSE:
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)

}
5 changes: 4 additions & 1 deletion code.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ func (c code) Error() string {

if bytes.Equal(c, []byte{0x6a, 0x80}) {
Copy link
Owner

Choose a reason for hiding this comment

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

Here you did not introduce constants? Could you also include the constant documentation links while at it (in constants.go?)?

Copy link
Author

Choose a reason for hiding this comment

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

I only included the constants listed at the top of https://developers.yubico.com/OATH/YKOATH_Protocol.html

Although this 0x6a80 is not listed as a constant there, it appears to be consistent throughout the documentation. I'll take a look through and include those and make a note along with the documentation link.

Copy link
Author

Choose a reason for hiding this comment

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

On a similar note, rather than returning a string, I could make this return an error for each of these. What do you think?

return "wrong syntax"
} else if bytes.Equal(c, []byte{0x69, 0x82}) {
return "requires auth"
} else if bytes.Equal(c, []byte{0x69, 0x84}) {
return "no such object"
} else if bytes.Equal(c, []byte{0x65, 0x81}) {
return "generic error"
}

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

}

// IsMore indicates more data that needs to be fetched
Expand Down
49 changes: 49 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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.


const (
// Instructions
INST_PUT = 0x01 // Requires auth
INST_DELETE = 0x02 // Requires auth
INST_SET_CODE = 0x03 // Requires auth
INST_RESET = 0x04
INST_LIST = 0xa1 // Requires auth
INST_CALCULATE = 0xa2 // Requires auth
INST_VALIDATE = 0xa3
INST_CALCULATE_ALL = 0xa4 // Requires auth
INST_SEND_REMAINING = 0xa5 // Requires auth

// Response size
RS_FULL_RESPONSE = 0x00
RS_TRUNCATED_RESPONSE = 0x01

// Algorithms
A_HMAC_SHA1 = 0x01
A_HMAC_SHA256 = 0x02
A_HMAC_SHA512 = 0x03

// OATH Types
OT_HOTP = 0x10
OT_TOTP = 0x20

// Properties
PROP_INCREASING = 0x01
PROP_REQUIRE_TOUCH = 0x02

// Tags
TAG_NAME = 0x71
TAG_NAME_LIST = 0x72
TAG_KEY = 0x73
TAG_CHALLENGE = 0x74
TAG_RESPONSE = 0x75
TAG_TRUNCATED_RESPONSE = 0x76
TAG_NO_RESPONSE = 0x77
TAG_PROPERTY = 0x78
TAG_VERSION = 0x79
TAG_IMF = 0x7a
TAG_ALGORITHM = 0x7b
TAG_TOUCH = 0x7c

// Mask
MASK_ALGO = 0x0f
MASK_TYPE = 0xf0
)
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, INST_DELETE, 0x00, 0x00,
write(TAG_NAME, []byte(name)))

return err

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, INST_LIST, 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 TAG_NAME_LIST:

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

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 = (MASK_TYPE|byte(a))&MASK_ALGO | byte(t)
dig = byte(digits)
prp []byte
)

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

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

Expand Down
54 changes: 49 additions & 5 deletions select.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package ykoath

import (
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"fmt"
"golang.org/x/crypto/pbkdf2"
"hash"
"strings"
)

// Select encapsulates the results of the "SELECT" instruction
Expand All @@ -12,6 +19,44 @@ type Select struct {
Version []byte
}

// Salt returns the selected salt
func (s Select) Salt() []byte {
return s.Name
}

// Hash returns a Hash constructor for the algorithm
func (s Select) Hash() (func() hash.Hash, error) {
// If no agorithm found, default to sha1
if len(s.Algorithm) == 0 {
return sha1.New, nil
}
switch s.Algorithm[0] {
case A_HMAC_SHA1:
return sha1.New, nil
case A_HMAC_SHA256:
return sha256.New, nil
case A_HMAC_SHA512:
return sha512.New, nil
}
return sha1.New, fmt.Errorf("unknown hash algoritm %x", s.Algorithm)
}

// DeviceID returns the selected device ID
func (s Select) DeviceID() string {
h := sha256.New()
h.Write(s.Salt())
sum := h.Sum(nil)
sum = sum[:16]
return strings.Replace(base64.StdEncoding.EncodeToString(sum), "=", "", -1)
}

// DeriveKey returns a key as a byte array from a given passphrase
func (s Select) DeriveKey(passphrase string) []byte {
iters := 1000
keyLength := 16
return pbkdf2.Key([]byte(passphrase), s.Salt(), iters, keyLength, sha1.New)
}

// Select sends a "SELECT" instruction, initializing the device for an OATH session
func (o *OATH) Select() (*Select, error) {

Expand All @@ -26,15 +71,14 @@ func (o *OATH) Select() (*Select, error) {
s := new(Select)

for _, tv := range res {

switch tv.tag {
case 0x7b:
case TAG_ALGORITHM:
s.Algorithm = tv.value
case 0x74:
case TAG_CHALLENGE:
s.Challenge = tv.value
case 0x71:
case TAG_NAME:
s.Name = tv.value
case 0x79:
case TAG_VERSION:
s.Version = tv.value
default:
return nil, fmt.Errorf(errUnknownTag, tv.tag)
Expand Down
Loading