diff --git a/.golangci.yaml b/.golangci.yaml index fb0ac4e..8a5753a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -35,7 +35,7 @@ linters-settings: sections: - standard - default - - prefix(cunicu.li/cunicu) + - prefix(cunicu.li/go-ykoath) - blank - dot diff --git a/.reuse/dep5 b/.reuse/dep5 index 1842092..cee7072 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -3,6 +3,6 @@ Upstream-Name: go-ykoath Upstream-Contact: Steffen Vogel Source: https://cunicu.li -Files: go.sum CHANGELOG.md README.md +Files: go.sum CHANGELOG.md README.md mockdata/** Copyright: 2018 Joern Barthel License: Apache-2.0 diff --git a/README.md b/README.md index 11df17e..6523cfe 100644 --- a/README.md +++ b/README.md @@ -19,24 +19,24 @@ The package `ykoath` implements the YubiKey [YKOATH protocol](https://developers ## Usage ```go -oath, err := ykoath.New() +c, err := ykoath.New() if err != nil { log.Fatal(err) } -defer oath.Close() +defer c.Close() -if _, err = oath.Select(); err != nil { +if _, err = c.Select(); err != nil { log.Fatalf("Failed to select app: %v", err) } -names, err := oath.List() +names, err := c.List() if err != nil { log.Fatal("Failed to list slots: %v", err) } for _, name := range names { - calc, err := oath.Calculate(name.Name, func(name string) error { + calc, err := c.Calculate(name.Name, func(name string) error { log.Printf("*** Please touch your YubiKey to unlock slot: %q ***", name) return nil }) @@ -47,11 +47,11 @@ for _, name := range names { log.Printf("Got one-time-password %s for slot %q", calc, name) } -if err := oath.Put("test", ykoath.HmacSha1, ykoath.Totp, 6, []byte("open sesame"), true); err != nil { +if err := c.Put("test", ykoath.HmacSha1, ykoath.Totp, 6, []byte("open sesame"), true); err != nil { log.Fatal(err) } -if err := oath.Put("test2", ykoath.HmacSha1, ykoath.Totp, 6, []byte("open sesame"), true); err != nil { +if err := c.Put("test2", ykoath.HmacSha1, ykoath.Totp, 6, []byte("open sesame"), true); err != nil { log.Fatal(err) } ``` diff --git a/calculate.go b/calculate.go index 713e87f..bbcac04 100644 --- a/calculate.go +++ b/calculate.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2018 Joern Barthel +// SPDX-FileCopyrightText: 2023 Steffen Vogel // SPDX-License-Identifier: Apache-2.0 package ykoath @@ -8,30 +8,37 @@ import ( "errors" "fmt" "strings" + + "cunicu.li/go-iso7816/encoding/tlv" ) var ( - errNoValuesFound = errors.New("no values found in response") - errUnknownName = errors.New("no such name configured") - errMultipleMatches = errors.New("multiple matches found") - errTouchRequired = errors.New("touch-required") + ErrNoValuesFound = errors.New("no values found in response") + ErrUnknownName = errors.New("no such name configured") + ErrMultipleMatches = errors.New("multiple matches found") + ErrTouchRequired = errors.New("touch required") + ErrTouchCallbackRequired = errors.New("touch callback required") + ErrChallengeRequired = errors.New("challenge required") ) // 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(o.totpChallenge(), true) +func (c *Card) Calculate(name string, touchRequiredCallback func(string) error) (string, error) { + totpChallenge := c.totpChallenge() + + codes, err := c.calculateAll(totpChallenge, true) if err != nil { return "", err } // Support matching by name without issuer in the same way that ykman does // https://github.com/Yubico/yubikey-manager/blob/f493008d78a0ad09016f23dabd1cb658929d9c0e/ykman/cli/oath.py#L543 - var key, code string + var key string + var code Code var matches []string - for k, c := range res { + for k, c := range codes { if strings.Contains(strings.ToLower(k), strings.ToLower(name)) { key = k code = c @@ -39,72 +46,94 @@ func (o *OATH) Calculate(name string, touchRequiredCallback func(string) error) } } if len(matches) > 1 { - return "", fmt.Errorf("%w: %s", errMultipleMatches, strings.Join(matches, ",")) + return "", fmt.Errorf("%w: %s", ErrMultipleMatches, strings.Join(matches, ",")) } if key == "" { - return "", fmt.Errorf("%w: %s", errUnknownName, name) + return "", fmt.Errorf("%w: %s", ErrUnknownName, name) } - if code == errTouchRequired.Error() { - if err := touchRequiredCallback(name); err != nil { - return "", err + if code.TouchRequired || code.Type == Hotp { + if code.TouchRequired { + if touchRequiredCallback == nil { + return "", ErrTouchCallbackRequired + } + + if err := touchRequiredCallback(key); err != nil { + return "", err + } } - pw, digits, err := o.calculate(key, o.totpChallenge(), true) - if err != nil { - return "", err + var challenge []byte + if code.Type == Totp { + challenge = totpChallenge } - return otp(digits, pw), nil + if code, err = c.calculate(key, challenge, true); err != nil { + return "", err + } } - return code, nil + return code.OTP(), nil } -func (o *OATH) CalculateTOTP(name string) ([]byte, int, error) { - return o.CalculateHOTP(name, o.totpChallenge()) +func (c *Card) CalculateDirect(name string) (string, error) { + d, err := c.calculate(name, c.totpChallenge(), true) + if err != nil { + return "", err + } + + return d.OTP(), nil } -func (o *OATH) CalculateHOTP(name string, challenge []byte) ([]byte, int, error) { - return o.calculate(name, challenge, false) +func (c *Card) CalculateRaw(name string, challenge []byte) ([]byte, int, error) { + d, err := c.calculate(name, challenge, false) + if err != nil { + return nil, -1, err + } + + return d.Hash, d.Digits, nil } // calculate implements the "CALCULATE" instruction -func (o *OATH) calculate(name string, challenge []byte, truncate bool) ([]byte, int, error) { +func (c *Card) calculate(name string, challenge []byte, truncate bool) (Code, error) { var trunc byte if truncate { trunc = 0x01 } - res, err := o.send(0x00, insCalculate, 0x00, trunc, - write(tagName, []byte(name)), - write(tagChallenge, challenge), + tvs, err := c.send(insCalculate, 0x00, trunc, + tlv.New(tagName, []byte(name)), + tlv.New(tagChallenge, challenge), ) if err != nil { - return nil, 0, err + return Code{}, err } - for _, tv := range res { - switch tv.tag { + for _, tv := range tvs { + switch tv.Tag { case tagResponse, tagTruncated: - digits := int(tv.value[0]) - hash := tv.value[1:] - return hash, digits, nil + digits := int(tv.Value[0]) + hash := tv.Value[1:] + return Code{ + Hash: hash, + Digits: digits, + Truncated: tv.Tag == tagTruncated, + }, nil default: - return nil, 0, fmt.Errorf("%w: %x", errUnknownTag, tv.tag) + return Code{}, fmt.Errorf("%w: %x", errUnknownTag, tv.Tag) } } - return nil, 0, fmt.Errorf("%w: %x", errNoValuesFound, res) + return Code{}, ErrNoValuesFound } // 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(challenge []byte, truncate bool) (map[string]string, error) { +func (c *Card) calculateAll(challenge []byte, truncate bool) (map[string]Code, error) { var ( - codes []string + codes []Code names []string trunc byte @@ -114,32 +143,43 @@ func (o *OATH) calculateAll(challenge []byte, truncate bool) (map[string]string, trunc = 0x01 } - res, err := o.send(0x00, insCalculateAll, 0x00, trunc, - write(tagChallenge, challenge), + tvs, err := c.send(insCalculateAll, 0x00, trunc, + tlv.New(tagChallenge, challenge), ) if err != nil { return nil, err } - for _, tv := range res { - switch tv.tag { + for _, tv := range tvs { + switch tv.Tag { case tagName: - names = append(names, string(tv.value)) + names = append(names, string(tv.Value)) case tagTouch: - codes = append(codes, errTouchRequired.Error()) + codes = append(codes, Code{ + Type: Totp, + TouchRequired: true, + }) case tagResponse, tagTruncated: - digits := int(tv.value[0]) - hash := tv.value[1:] - codes = append(codes, otp(digits, hash)) + codes = append(codes, Code{ + Type: Totp, + Hash: tv.Value[1:], + Digits: int(tv.Value[0]), + Truncated: tv.Tag == tagTruncated, + }) + + case tagHOTP: + codes = append(codes, Code{ + Type: Hotp, + }) default: - return nil, fmt.Errorf("%w (%#x)", errUnknownTag, tv.tag) + return nil, fmt.Errorf("%w (%#x)", errUnknownTag, tv.Tag) } } - all := make(map[string]string, len(names)) + all := make(map[string]Code, len(names)) for idx, name := range names { all[name] = codes[idx] @@ -148,13 +188,7 @@ func (o *OATH) calculateAll(challenge []byte, truncate bool) (map[string]string, return all, nil } -func (o *OATH) totpChallenge() []byte { - counter := o.Clock().Unix() / int64(o.Timestep.Seconds()) +func (c *Card) totpChallenge() []byte { + counter := c.Clock().Unix() / int64(c.Timestep.Seconds()) return binary.BigEndian.AppendUint64(nil, uint64(counter)) } - -// otp converts a value into a (6 or 8 digits) one-time password -func otp(digits int, hash []byte) string { - code := binary.BigEndian.Uint32(hash) - return fmt.Sprintf(fmt.Sprintf("%%0%dd", digits), code) -} diff --git a/calculate_test.go b/calculate_test.go index 1812ad8..37791f4 100644 --- a/calculate_test.go +++ b/calculate_test.go @@ -1,246 +1,154 @@ // SPDX-FileCopyrightText: 2018 Joern Barthel // SPDX-License-Identifier: Apache-2.0 -package ykoath +package ykoath_test import ( + "errors" "testing" - "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cunicu.li/go-ykoath" ) func TestCalculate(t *testing.T) { - assert := assert.New(t) - - for idx, k := range keys { - var ( - testCard = new(testCard) - touched = false - v = vectors[k] - ) - - testCard. - On( - "Transmit", - []byte{ - 0x00, 0xa4, 0x00, 0x01, 0x0a, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, - }). - Return( - []byte{ //nolint:dupl // false-positive - 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x31, 0x2d, 0x31, 0x65, - 0x35, 0x66, 0x32, 0x64, 0x62, 0x39, 0x2d, 0x34, 0x37, 0x37, 0x65, 0x2d, - 0x34, 0x31, 0x61, 0x66, 0x2d, 0x62, 0x64, 0x32, 0x65, 0x2d, 0x36, 0x30, - 0x62, 0x63, 0x35, 0x36, 0x39, 0x61, 0x65, 0x38, 0x37, 0x31, 0x76, 0x05, - 0x06, 0x00, 0x04, 0x61, 0x6a, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, - 0x30, 0x32, 0x2d, 0x32, 0x61, 0x37, 0x63, 0x62, 0x63, 0x61, 0x39, 0x2d, - 0x62, 0x61, 0x65, 0x66, 0x2d, 0x34, 0x37, 0x65, 0x33, 0x2d, 0x38, 0x63, - 0x65, 0x38, 0x2d, 0x37, 0x38, 0x38, 0x62, 0x63, 0x36, 0x38, 0x35, 0x33, - 0x65, 0x31, 0x32, 0x7c, 0x01, 0x06, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, - 0x2d, 0x30, 0x33, 0x2d, 0x62, 0x30, 0x31, 0x30, 0x31, 0x39, 0x65, 0x64, - 0x2d, 0x32, 0x61, 0x66, 0x31, 0x2d, 0x34, 0x38, 0x63, 0x63, 0x2d, 0x61, - 0x36, 0x34, 0x63, 0x2d, 0x66, 0x61, 0x39, 0x62, 0x34, 0x32, 0x34, 0x64, - 0x62, 0x39, 0x39, 0x33, 0x76, 0x05, 0x06, 0x00, 0x0a, 0x96, 0xb0, 0x71, - 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x34, 0x2d, 0x65, 0x36, 0x32, - 0x31, 0x37, 0x31, 0x66, 0x30, 0x2d, 0x34, 0x63, 0x66, 0x36, 0x2d, 0x34, - 0x39, 0x39, 0x65, 0x2d, 0x62, 0x39, 0x38, 0x38, 0x2d, 0x36, 0x65, 0x66, - 0x33, 0x36, 0x62, 0x32, 0x31, 0x33, 0x63, 0x63, 0x36, 0x7c, 0x01, 0x06, - 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x35, 0x2d, 0x34, 0x35, - 0x38, 0x61, 0x66, 0x39, 0x65, 0x65, 0x2d, 0x63, 0x61, 0x61, 0x61, 0x2d, - 0x34, 0x37, 0x31, 0x36, 0x2d, 0x62, 0x66, 0x62, 0x38, 0x2d, 0x62, 0x64, - 0x38, 0x32, 0x38, 0x37, 0x35, 0x37, 0x39, 0x35, 0x35, 0x64, 0x76, 0x05, - 0x06, 0x00, 0x61, 0xff, - }, - nil, - ).Once(). - On( - "Transmit", - []byte{ - 0x00, 0xa5, 0x00, 0x00, - }). - Return( - []byte{ //nolint:dupl // false-positive - 0x01, 0xd1, 0xce, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x36, - 0x2d, 0x32, 0x31, 0x33, 0x38, 0x61, 0x39, 0x39, 0x31, 0x2d, 0x65, 0x63, - 0x37, 0x30, 0x2d, 0x34, 0x38, 0x63, 0x62, 0x2d, 0x38, 0x33, 0x65, 0x36, - 0x2d, 0x66, 0x38, 0x30, 0x64, 0x61, 0x34, 0x37, 0x63, 0x39, 0x33, 0x65, - 0x34, 0x7c, 0x01, 0x06, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, - 0x37, 0x2d, 0x61, 0x37, 0x30, 0x61, 0x32, 0x35, 0x32, 0x30, 0x2d, 0x37, - 0x65, 0x35, 0x31, 0x2d, 0x34, 0x35, 0x62, 0x32, 0x2d, 0x62, 0x61, 0x61, - 0x62, 0x2d, 0x30, 0x65, 0x33, 0x35, 0x32, 0x32, 0x30, 0x62, 0x30, 0x36, - 0x66, 0x65, 0x76, 0x05, 0x08, 0x05, 0x9e, 0xb4, 0xea, 0x71, 0x2c, 0x74, - 0x65, 0x73, 0x74, 0x2d, 0x30, 0x38, 0x2d, 0x38, 0x33, 0x66, 0x65, 0x33, - 0x32, 0x30, 0x38, 0x2d, 0x62, 0x31, 0x39, 0x32, 0x2d, 0x34, 0x36, 0x63, - 0x32, 0x2d, 0x39, 0x63, 0x62, 0x32, 0x2d, 0x31, 0x34, 0x65, 0x65, 0x39, - 0x31, 0x37, 0x62, 0x34, 0x64, 0x36, 0x30, 0x7c, 0x01, 0x08, 0x71, 0x2c, - 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x39, 0x2d, 0x63, 0x63, 0x39, 0x64, - 0x31, 0x32, 0x32, 0x65, 0x2d, 0x39, 0x62, 0x35, 0x31, 0x2d, 0x34, 0x33, - 0x35, 0x65, 0x2d, 0x62, 0x34, 0x38, 0x65, 0x2d, 0x61, 0x62, 0x31, 0x61, - 0x31, 0x37, 0x31, 0x35, 0x37, 0x65, 0x33, 0x63, 0x76, 0x05, 0x08, 0x05, - 0x67, 0xe1, 0x30, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x31, 0x30, - 0x2d, 0x39, 0x37, 0x61, 0x35, 0x38, 0x39, 0x33, 0x38, 0x2d, 0x38, 0x65, - 0x61, 0x36, 0x2d, 0x34, 0x31, 0x34, 0x33, 0x2d, 0x61, 0x65, 0x31, 0x30, - 0x2d, 0x38, 0x61, 0x64, 0x62, 0x39, 0x32, 0x62, 0x64, 0x63, 0x33, 0x33, - 0x35, 0x7c, 0x61, 0x68, - }, - nil, - ).Once(). - On( - "Transmit", - []byte{ - 0x00, 0xa5, 0x00, 0x00, - }). - Return( - []byte{ - 0x01, 0x08, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x31, 0x31, 0x2d, - 0x38, 0x38, 0x37, 0x66, 0x64, 0x33, 0x38, 0x62, 0x2d, 0x38, 0x30, 0x62, - 0x33, 0x2d, 0x34, 0x64, 0x37, 0x61, 0x2d, 0x38, 0x36, 0x37, 0x31, 0x2d, - 0x38, 0x32, 0x62, 0x65, 0x66, 0x36, 0x33, 0x31, 0x35, 0x31, 0x61, 0x36, - 0x76, 0x05, 0x08, 0x02, 0xbf, 0xb9, 0x4e, 0x71, 0x2c, 0x74, 0x65, 0x73, - 0x74, 0x2d, 0x31, 0x32, 0x2d, 0x64, 0x61, 0x65, 0x65, 0x35, 0x30, 0x64, - 0x31, 0x2d, 0x37, 0x62, 0x62, 0x66, 0x2d, 0x34, 0x31, 0x65, 0x36, 0x2d, - 0x61, 0x36, 0x35, 0x62, 0x2d, 0x64, 0x33, 0x34, 0x30, 0x34, 0x36, 0x64, - 0x62, 0x61, 0x32, 0x38, 0x37, 0x7c, 0x01, 0x08, 0x90, 0x00, - }, - nil, - ).Once() - - switch idx { - case 1: - - testCard.On( - "Transmit", - []byte{ - 0x00, 0xa2, 0x00, 0x01, 0x38, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, - 0x30, 0x32, 0x2d, 0x32, 0x61, 0x37, 0x63, 0x62, 0x63, 0x61, 0x39, 0x2d, - 0x62, 0x61, 0x65, 0x66, 0x2d, 0x34, 0x37, 0x65, 0x33, 0x2d, 0x38, 0x63, - 0x65, 0x38, 0x2d, 0x37, 0x38, 0x38, 0x62, 0x63, 0x36, 0x38, 0x35, 0x33, - 0x65, 0x31, 0x32, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, - }). - Return( - []byte{ - 0x76, 0x05, 0x06, 0x00, 0x01, 0xd1, 0xce, 0x90, 0x00, - }, - nil, - ).Once() - - case 3: - - testCard.On( - "Transmit", - []byte{ - 0x00, 0xa2, 0x00, 0x01, 0x38, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, - 0x30, 0x34, 0x2d, 0x65, 0x36, 0x32, 0x31, 0x37, 0x31, 0x66, 0x30, 0x2d, - 0x34, 0x63, 0x66, 0x36, 0x2d, 0x34, 0x39, 0x39, 0x65, 0x2d, 0x62, 0x39, - 0x38, 0x38, 0x2d, 0x36, 0x65, 0x66, 0x33, 0x36, 0x62, 0x32, 0x31, 0x33, - 0x63, 0x63, 0x36, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, - }). - Return( - []byte{ - 0x76, 0x05, 0x06, 0x00, 0x04, 0x61, 0x6a, 0x90, 0x00, - }, - nil, - ).Once() - - case 5: - - testCard.On( - "Transmit", - []byte{ - 0x00, 0xa2, 0x00, 0x01, 0x38, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, - 0x30, 0x36, 0x2d, 0x32, 0x31, 0x33, 0x38, 0x61, 0x39, 0x39, 0x31, 0x2d, - 0x65, 0x63, 0x37, 0x30, 0x2d, 0x34, 0x38, 0x63, 0x62, 0x2d, 0x38, 0x33, - 0x65, 0x36, 0x2d, 0x66, 0x38, 0x30, 0x64, 0x61, 0x34, 0x37, 0x63, 0x39, - 0x33, 0x65, 0x34, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, - }). - Return( - []byte{ - 0x76, 0x05, 0x06, 0x00, 0x0a, 0x96, 0xb0, 0x90, 0x00, - }, - nil, - ).Once() - - case 7: - - testCard.On( - "Transmit", - []byte{ - 0x00, 0xa2, 0x00, 0x01, 0x38, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, - 0x30, 0x38, 0x2d, 0x38, 0x33, 0x66, 0x65, 0x33, 0x32, 0x30, 0x38, 0x2d, - 0x62, 0x31, 0x39, 0x32, 0x2d, 0x34, 0x36, 0x63, 0x32, 0x2d, 0x39, 0x63, - 0x62, 0x32, 0x2d, 0x31, 0x34, 0x65, 0x65, 0x39, 0x31, 0x37, 0x62, 0x34, - 0x64, 0x36, 0x30, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, - }). - Return( - []byte{ - 0x76, 0x05, 0x08, 0x02, 0xbf, 0xb9, 0x4e, 0x90, 0x00, - }, - nil, - ).Once() - - case 9: - - testCard.On( - "Transmit", - []byte{ - 0x00, 0xa2, 0x00, 0x01, 0x38, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, - 0x31, 0x30, 0x2d, 0x39, 0x37, 0x61, 0x35, 0x38, 0x39, 0x33, 0x38, 0x2d, - 0x38, 0x65, 0x61, 0x36, 0x2d, 0x34, 0x31, 0x34, 0x33, 0x2d, 0x61, 0x65, - 0x31, 0x30, 0x2d, 0x38, 0x61, 0x64, 0x62, 0x39, 0x32, 0x62, 0x64, 0x63, - 0x33, 0x33, 0x35, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, - }). - Return( - []byte{ - 0x76, 0x05, 0x08, 0x05, 0x9e, 0xb4, 0xea, 0x90, 0x00, - }, - nil, - ).Once() - - case 11: - - testCard.On( - "Transmit", - []byte{ - 0x00, 0xa2, 0x00, 0x01, 0x38, 0x71, 0x2c, 0x74, 0x65, 0x73, 0x74, 0x2d, - 0x31, 0x32, 0x2d, 0x64, 0x61, 0x65, 0x65, 0x35, 0x30, 0x64, 0x31, 0x2d, - 0x37, 0x62, 0x62, 0x66, 0x2d, 0x34, 0x31, 0x65, 0x36, 0x2d, 0x61, 0x36, - 0x35, 0x62, 0x2d, 0x64, 0x33, 0x34, 0x30, 0x34, 0x36, 0x64, 0x62, 0x61, - 0x32, 0x38, 0x37, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, - }). - Return( - []byte{ - 0x76, 0x05, 0x08, 0x05, 0x67, 0xe1, 0x30, 0x90, 0x00, - }, - nil, - ).Once() - } + vs := vectorsTOTP[:3] + vs = append(vs, vectorsTOTP[:3]...) - client := &OATH{ - card: testCard, - Timestep: DefaultTimeStep, - Clock: func() time.Time { - return time.Unix(v.time, 0) - }, + withCard(t, vs, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + for _, v := range vs { + code, err := card.Calculate(v.Name, nil) + require.NoError(err) + require.Equal(v.Code, code) } + }) +} + +func TestCalculateMatchPartial(t *testing.T) { + withCard(t, nil, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + err := card.Put("testvector", ykoath.HmacSha1, ykoath.Totp, 8, testSecretSHA1, false, 0) + require.NoError(err) + + res, err := card.Calculate("test", nil) + require.NoError(err) + require.Equal("94287082", res) + }) +} - res, err := client.Calculate(k, func(_ string) error { - touched = true +func TestCalculateMatchFull(t *testing.T) { + withCard(t, nil, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + err := card.Put("testvector", ykoath.HmacSha1, ykoath.Totp, 8, testSecretSHA1, false, 0) + require.NoError(err) + + res, err := card.Calculate("testvector", nil) + require.NoError(err) + require.Equal("94287082", res) + }) +} + +func TestCalculateMatchMultiple(t *testing.T) { + withCard(t, nil, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + err := card.Put("testvector1", ykoath.HmacSha1, ykoath.Totp, 8, testSecretSHA1, false, 0) + require.NoError(err) + + err = card.Put("testvector2", ykoath.HmacSha1, ykoath.Totp, 8, testSecretSHA1, false, 0) + require.NoError(err) + + _, err = card.Calculate("test", nil) + require.ErrorIs(err, ykoath.ErrMultipleMatches) + }) +} + +func TestCalculateRequireTouch(t *testing.T) { + withCard(t, []vector{ + { + Name: "touch required", + Alg: ykoath.HmacSha256, + Typ: ykoath.Totp, + Digits: 6, + Secret: fromHex("12341234"), + Touch: true, + }, + }, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + // Callback missing + _, err := card.Calculate("touch", nil) + require.ErrorIs(err, ykoath.ErrTouchCallbackRequired) + + // Error raised in callback + _, err = card.Calculate("touch", func(s string) error { + return errors.New("my error") //nolint:goerr113 + }) + require.ErrorContains(err, "my error") + + // Callback called but button not pressed + touchRequested := false + _, err = card.Calculate("touch", func(s string) error { + require.Equal(s, "touch required") + touchRequested = true return nil }) - assert.NoError(err) + require.NoError(err) + require.True(touchRequested) + }) +} + +func TestCalculateTOTP(t *testing.T) { + v := vectorsTOTP[0] + withCard(t, []vector{v}, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + code, err := card.CalculateDirect(v.Name) + require.NoError(err) + require.Equal(v.Code, code) + }) +} + +func TestCalculateHOTPCounterIncrement(t *testing.T) { + v := vectorsHOTP[0] + withCard(t, []vector{v}, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) - if v.touch { - assert.True(touched) + for _, ev := range vectorsHOTP[:10] { + code, err := card.CalculateDirect(v.Name) + require.NoError(err) + require.Equal(ev.Code, code) } + }) +} + +func TestCalculateHOTPCounterInit(t *testing.T) { + withCard(t, vectorsHOTP, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + for _, v := range vectorsHOTP { + code, err := card.CalculateDirect(v.Name) + require.NoError(err) + require.Equal(v.Code, code) + } + }) +} + +func TestCalculateRAW(t *testing.T) { + expResp := fromHex("28c6d33a03e7c67940c30d06253f8980f8ef54bd") + + v := vectorsTOTP[0] + withCard(t, []vector{v}, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) - assert.Equal(v.testvector, res) - testCard.AssertExpectations(t) - } + resp, _, err := card.CalculateRaw(v.Name, fromString("hallo")) + require.NoError(err) + require.Equal(expResp, resp) + require.Len(resp, v.Alg.Hash()().Size()) + }) } diff --git a/code.go b/code.go index ed5e0a1..3caba52 100644 --- a/code.go +++ b/code.go @@ -1,137 +1,34 @@ -// SPDX-FileCopyrightText: 2018 Joern Barthel +// SPDX-FileCopyrightText: 2023 Steffen Vogel // SPDX-License-Identifier: Apache-2.0 package ykoath import ( + "encoding/binary" "fmt" ) -// code encapsulates (some) response codes from the spec -type code [2]byte - -var ( - errAuthRequired = code{0x69, 0x82} - errGeneric = code{0x65, 0x81} - errNoSpace = code{0x6a, 0x84} - errNoSuchObject = code{0x69, 0x84} - errResponseDoesNotMatch = code{0x69, 0x84} - errWrongSyntax = code{0x6a, 0x80} - - // Error codes from Nitrokeys Trussed Secrets App - // See: https://github.com/Nitrokey/pynitrokey/blob/abf42efeff7794ebc29de281c93b14003c475407/pynitrokey/nk3/secrets_app.py#L133C6-L133C6 - errMoreDataAvailable = code{0x61, 0xFF} - errVerificationFailed = code{0x63, 0x00} - errUnspecifiedNonpersistentExecutionError = code{0x64, 0x00} - errUnspecifiedPersistentExecutionError = code{0x65, 0x00} - errWrongLength = code{0x67, 0x00} - errLogicalChannelNotSupported = code{0x68, 0x81} - errSecureMessagingNotSupported = code{0x68, 0x82} - errCommandChainingNotSupported = code{0x68, 0x84} - errSecurityStatusNotSatisfied = code{0x69, 0x82} - errConditionsOfUseNotSatisfied = code{0x69, 0x85} - errOperationBlocked = code{0x69, 0x83} - errIncorrectDataParameter = code{0x6A, 0x80} - errFunctionNotSupported = code{0x6A, 0x81} - errNotFound = code{0x6A, 0x82} - errNotEnoughMemory = code{0x6A, 0x84} - errIncorrectP1OrP2Parameter = code{0x6A, 0x86} - errKeyReferenceNotFound = code{0x6A, 0x88} - errInstructionNotSupportedOrInvalid = code{0x6D, 0x00} - errClassNotSupported = code{0x6E, 0x00} - errUnspecifiedCheckingError = code{0x6F, 0x00} - errSuccess = code{0x90, 0x00} -) - -// Error return the encapsulated error string -func (c code) Error() string { - switch c { - case errAuthRequired: - return "authentication required" - - case errGeneric: - return "generic error" - - case errNoSpace: - return "no space" - - case errNoSuchObject: - return "no such object" - - case errResponseDoesNotMatch: - return "response does not match" - - case errWrongSyntax: - return "wrong syntax" - - case errMoreDataAvailable: - return "more data available" - - case errVerificationFailed: - return "verification failed" - - case errUnspecifiedNonpersistentExecutionError: - return "unspecified non-persistent execution error" - - case errUnspecifiedPersistentExecutionError: - return "unspecified persistent execution error" - - case errWrongLength: - return "wrong length" - - case errLogicalChannelNotSupported: - return "logical channel not supported" - - case errSecureMessagingNotSupported: - return "secure messaging not supported" - - case errCommandChainingNotSupported: - return "command chaining not supported" - - case errSecurityStatusNotSatisfied: - return "security status not satisfied" - - case errConditionsOfUseNotSatisfied: - return "conditions of use not satisfied" - - case errOperationBlocked: - return "operation blocked" - - case errIncorrectDataParameter: - return "incorrect data parameter" - - case errFunctionNotSupported: - return "function not supported" - - case errNotFound: - return "not found" - - case errNotEnoughMemory: - return "not enough memory" - - case errIncorrectP1OrP2Parameter: - return "incorrect p1/p2 param" - - case errKeyReferenceNotFound: - return "key reference not found" - - case errInstructionNotSupportedOrInvalid: - return "instruction not supported or invalid" - - case errClassNotSupported: - return "class not supported" - - case errUnspecifiedCheckingError: - return "unspecified checking error" +type Code struct { + Hash []byte + Digits int + Type Type + TouchRequired bool + Truncated bool +} - case errSuccess: - return "success" +// OTP converts a value into a (6 or 8 digits) one-time password +// See: RFC 4226 Section 5.3 - Generating an HOTP Value +// https://datatracker.ietf.org/doc/html/rfc4226#section-5.3 +func (c Code) OTP() string { + var code uint32 + if c.Truncated { + code = binary.BigEndian.Uint32(c.Hash) + } else { + hl := len(c.Hash) + o := c.Hash[hl-1] & 0xf + code = binary.BigEndian.Uint32(c.Hash[o:o+4]) & ^uint32(1<<31) } - return fmt.Sprintf("unknown (0x%x%x)", c[0], c[1]) -} - -// IsMore indicates more data that needs to be fetched -func (c code) IsMore() bool { - return c[0] == 0x61 + s := fmt.Sprintf("%08d", code) + return s[len(s)-c.Digits:] } diff --git a/code_test.go b/code_test.go new file mode 100644 index 0000000..574987c --- /dev/null +++ b/code_test.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2018 Joern Barthel +// SPDX-License-Identifier: Apache-2.0 + +package ykoath_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cunicu.li/go-ykoath" +) + +func TestOTP(t *testing.T) { + require := require.New(t) + for _, v := range vectors["HOTP"] { + c := ykoath.Code{ + Hash: v.Hash, + Digits: v.Digits, + } + + require.Equal(v.Code, c.OTP()) + } +} diff --git a/credential.go b/credential.go new file mode 100644 index 0000000..2562aa6 --- /dev/null +++ b/credential.go @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package ykoath + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +var ErrMalformedCredential = errors.New("malformed credential") + +var credRegex = regexp.MustCompile(`^((?P\d+)/)?((?P[^:]+):)?(?P.+)$`) + +type Credential struct { + TimeStep time.Duration + Name string + Issuer string +} + +func (c Credential) String() string { + return fmt.Sprintf("%s: %s", c.Issuer, c.Name) +} + +func (c Credential) Marshal() []byte { + s := "" + + if c.TimeStep != DefaultTimeStep { + s += fmt.Sprintf("%d/", c.TimeStep/time.Second) + } + + if c.Issuer != "" { + s += c.Issuer + ":" + } + + s += c.Name + + return []byte(s) +} + +func (c *Credential) Unmarshal(b []byte, t Type) error { + s := string(b) + + if t == Hotp { + if parts := strings.SplitN(s, ":", 2); len(parts) > 1 { + c.Issuer = parts[0] + c.Name = parts[1] + } else { + c.Issuer = "" + c.Name = parts[0] + } + + c.TimeStep = 0 + + return nil + } + + m := credRegex.FindStringSubmatch(s) + if m != nil { + if m[2] != "" { + ts, err := strconv.Atoi(m[2]) + if err != nil { + return err + } + + c.TimeStep = time.Second * time.Duration(ts) + } else { + c.TimeStep = DefaultTimeStep + } + + c.Issuer = m[4] + c.Name = m[5] + + return nil + } + + c.Issuer = "" + c.Name = s + c.TimeStep = DefaultTimeStep + + return nil +} diff --git a/credential_test.go b/credential_test.go new file mode 100644 index 0000000..3bf0dd2 --- /dev/null +++ b/credential_test.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package ykoath_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + yk "cunicu.li/go-ykoath" +) + +func TestCredential(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + Data []byte + Type yk.Type + Expected yk.Credential + }{ + { + Data: []byte("test"), + Type: yk.Totp, + Expected: yk.Credential{ + Issuer: "", + Name: "test", + TimeStep: yk.DefaultTimeStep, + }, + }, + { + Data: []byte("testIssuer:testName"), + Type: yk.Totp, + Expected: yk.Credential{ + Issuer: "testIssuer", + Name: "testName", + TimeStep: yk.DefaultTimeStep, + }, + }, + { + Data: []byte("45/testIssuer:testName"), + Type: yk.Totp, + Expected: yk.Credential{ + Issuer: "testIssuer", + Name: "testName", + TimeStep: 45 * time.Second, + }, + }, + { + Data: []byte("45/testName"), + Type: yk.Totp, + Expected: yk.Credential{ + Issuer: "", + Name: "testName", + TimeStep: 45 * time.Second, + }, + }, + } + + for _, tc := range cases { + var cred yk.Credential + + err := cred.Unmarshal(tc.Data, tc.Type) + assert.NoError(err) + + assert.Equal(tc.Expected, cred) + + data := cred.Marshal() + assert.Equal(tc.Data, data, "Got: %s", string(data)) + } +} diff --git a/delete.go b/delete.go index 5bd125a..c1b250c 100644 --- a/delete.go +++ b/delete.go @@ -3,10 +3,14 @@ package ykoath -// Delete sends a "DELETE" instruction, removing one named OATH credential -func (o *OATH) Delete(name string) error { - _, err := o.send(0x00, insDelete, 0x00, 0x00, - write(tagName, []byte(name))) +import ( + "cunicu.li/go-iso7816/encoding/tlv" +) +// Delete sends a "DELETE" instruction, removing one named OATH credential +func (c *Card) Delete(name string) error { + _, err := c.send(insDelete, 0x00, 0x00, + tlv.New(tagName, []byte(name)), + ) return err } diff --git a/delete_test.go b/delete_test.go new file mode 100644 index 0000000..02cbaf6 --- /dev/null +++ b/delete_test.go @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package ykoath_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cunicu.li/go-ykoath" +) + +func TestDelete(t *testing.T) { + vs := vectorsTOTP[:1] + withCard(t, vs, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + err := card.Delete(vs[0].Name) + require.NoError(err) + + creds, err := card.List() + require.NoError(err) + require.Len(creds, 0) + }) +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..dac2397 --- /dev/null +++ b/error.go @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package ykoath + +import ( + iso "cunicu.li/go-iso7816" +) + +type Error iso.Code + +var ( + ErrAuthRequired = Error{0x69, 0x82} + ErrGeneric = Error{0x65, 0x81} + ErrNoSpace = Error{0x6a, 0x84} + ErrNoSuchObject = Error{0x69, 0x84} + ErrResponseDoesNotMatch = Error{0x69, 0x84} + ErrWrongSyntax = Error{0x6a, 0x80} +) + +// Error return the encapsulated error string +func (e Error) Error() string { + switch e { + case ErrAuthRequired: + return "authentication required" + + case ErrGeneric: + return "generic error" + + case ErrNoSpace: + return "no space" + + case ErrNoSuchObject: + return "no such object" + + case ErrResponseDoesNotMatch: + return "response does not match" + + case ErrWrongSyntax: + return "wrong syntax" + + default: + c := iso.Code(e) + return c.Error() + } +} + +// IsMore indicates more data that needs to be fetched +func (e Error) HasMore() bool { + return iso.Code(e).HasMore() +} + +func wrapError(err error) error { + if err, ok := err.(iso.Code); ok { //nolint:errorlint + return Error(err) + } + + return err +} diff --git a/examples_test.go b/examples_test.go index 543d31b..33cd99e 100644 --- a/examples_test.go +++ b/examples_test.go @@ -3,41 +3,65 @@ //go:build !ci -package ykoath +package ykoath_test import ( "fmt" "log" "time" + + yk "cunicu.li/go-iso7816/devices/yubikey" + "cunicu.li/go-iso7816/drivers/pcsc" + "github.com/ebfe/scard" + + "cunicu.li/go-ykoath" ) func Example() { - oath, err := New() + ctx, err := scard.EstablishContext() + if err != nil { + log.Printf("Failed to establish context: %v", err) + return + } + + sc, err := pcsc.OpenFirstCard(ctx, yk.HasOATH) + if err != nil { + log.Printf("Failed to connect to card: %v", err) + return + } + + c, err := ykoath.NewCard(sc) if err != nil { log.Print(err) return } - defer oath.Close() + defer c.Close() // Fix the clock - oath.Clock = func() time.Time { + c.Clock = func() time.Time { return time.Unix(59, 0) } // Enable OATH for this session - if _, err = oath.Select(); err != nil { - log.Printf("Failed to select app: %v", err) + if _, err = c.Select(); err != nil { + log.Printf("Failed to select applet: %v", err) return } + // Reset the applet + // if err := c.Reset(); err != nil { + // log.Printf("Failed to reset applet: %v", err) + // return + // } + // Add the testvector - if err = oath.Put("testvector", HmacSha1, Totp, 8, []byte("12345678901234567890"), false); err != nil { + if err = c.Put("testvector", ykoath.HmacSha1, ykoath.Totp, 8, []byte("12345678901234567890"), false, 0); err != nil { log.Printf("Failed to put: %v", err) return } - names, err := oath.List() + names, err := c.List() if err != nil { log.Printf("Failed to list: %v", err) return @@ -47,7 +71,7 @@ func Example() { fmt.Printf("Name: %s\n", name) } - otp, _ := oath.Calculate("testvector", nil) + otp, _ := c.Calculate("testvector", nil) fmt.Printf("OTP: %s\n", otp) // Output: diff --git a/go.mod b/go.mod index a10ea18..cc5cbb4 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,14 @@ module cunicu.li/go-ykoath go 1.21.0 require ( - github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 + cunicu.li/go-iso7816 v0.1.0 golang.org/x/crypto v0.14.0 ) -require github.com/stretchr/testify v1.8.4 // test-only +require ( + github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 // test-only + github.com/stretchr/testify v1.8.4 // test-only +) require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 7cf841e..f4f2e93 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cunicu.li/go-iso7816 v0.1.0 h1:nGRDZaJX6rQOzKNhs4s6U1/CUJsFuRY7LPWjPsGKp8s= +cunicu.li/go-iso7816 v0.1.0/go.mod h1:a2zBW9JA2tqQAqk5rrFTcOH/wYh40+10KSJDPlNRCfM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/list.go b/list.go index 59fe1d4..148a72b 100644 --- a/list.go +++ b/list.go @@ -20,27 +20,27 @@ func (n *Name) String() string { } // List sends a "LIST" instruction, return a list of OATH credentials -func (o *OATH) List() ([]*Name, error) { +func (c *Card) List() ([]*Name, error) { var names []*Name - res, err := o.send(0x00, insList, 0x00, 0x00) + tvs, err := c.send(insList, 0x00, 0x00) if err != nil { return nil, err } - for _, tv := range res { - switch tv.tag { + for _, tv := range tvs { + switch tv.Tag { case tagNameList: name := &Name{ - Algorithm: Algorithm(tv.value[0] & 0x0f), - Name: string(tv.value[1:]), - Type: Type(tv.value[0] & 0xf0), + Algorithm: Algorithm(tv.Value[0] & 0x0f), + Name: string(tv.Value[1:]), + Type: Type(tv.Value[0] & 0xf0), } names = append(names, name) default: - return nil, fmt.Errorf("%w (%#x)", errUnknownTag, tv.tag) + return nil, fmt.Errorf("%w (%#x)", errUnknownTag, tv.Tag) } } diff --git a/list_test.go b/list_test.go index 52ce82b..8d3d0f3 100644 --- a/list_test.go +++ b/list_test.go @@ -1,117 +1,38 @@ // SPDX-FileCopyrightText: 2018 Joern Barthel // SPDX-License-Identifier: Apache-2.0 -package ykoath +package ykoath_test import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cunicu.li/go-ykoath" ) func TestList(t *testing.T) { - var ( - assert = assert.New(t) - testCard = new(testCard) - ) - - testCard. - On( - "Transmit", - []byte{ - 0x00, 0xa1, 0x00, 0x00, - }). - Return( - []byte{ //nolint:dupl // false-positive - 0x72, 0x2d, 0x21, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x31, 0x2d, 0x31, - 0x65, 0x35, 0x66, 0x32, 0x64, 0x62, 0x39, 0x2d, 0x34, 0x37, 0x37, 0x65, - 0x2d, 0x34, 0x31, 0x61, 0x66, 0x2d, 0x62, 0x64, 0x32, 0x65, 0x2d, 0x36, - 0x30, 0x62, 0x63, 0x35, 0x36, 0x39, 0x61, 0x65, 0x38, 0x37, 0x31, 0x72, - 0x2d, 0x22, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x32, 0x2d, 0x32, 0x61, - 0x37, 0x63, 0x62, 0x63, 0x61, 0x39, 0x2d, 0x62, 0x61, 0x65, 0x66, 0x2d, - 0x34, 0x37, 0x65, 0x33, 0x2d, 0x38, 0x63, 0x65, 0x38, 0x2d, 0x37, 0x38, - 0x38, 0x62, 0x63, 0x36, 0x38, 0x35, 0x33, 0x65, 0x31, 0x32, 0x72, 0x2d, - 0x23, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x33, 0x2d, 0x62, 0x30, 0x31, - 0x30, 0x31, 0x39, 0x65, 0x64, 0x2d, 0x32, 0x61, 0x66, 0x31, 0x2d, 0x34, - 0x38, 0x63, 0x63, 0x2d, 0x61, 0x36, 0x34, 0x63, 0x2d, 0x66, 0x61, 0x39, - 0x62, 0x34, 0x32, 0x34, 0x64, 0x62, 0x39, 0x39, 0x33, 0x72, 0x2d, 0x21, - 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x34, 0x2d, 0x65, 0x36, 0x32, 0x31, - 0x37, 0x31, 0x66, 0x30, 0x2d, 0x34, 0x63, 0x66, 0x36, 0x2d, 0x34, 0x39, - 0x39, 0x65, 0x2d, 0x62, 0x39, 0x38, 0x38, 0x2d, 0x36, 0x65, 0x66, 0x33, - 0x36, 0x62, 0x32, 0x31, 0x33, 0x63, 0x63, 0x36, 0x72, 0x2d, 0x22, 0x74, - 0x65, 0x73, 0x74, 0x2d, 0x30, 0x35, 0x2d, 0x34, 0x35, 0x38, 0x61, 0x66, - 0x39, 0x65, 0x65, 0x2d, 0x63, 0x61, 0x61, 0x61, 0x2d, 0x34, 0x37, 0x31, - 0x36, 0x2d, 0x62, 0x66, 0x62, 0x38, 0x2d, 0x62, 0x64, 0x38, 0x32, 0x38, - 0x37, 0x35, 0x37, 0x39, 0x35, 0x35, 0x64, 0x72, 0x2d, 0x23, 0x74, 0x65, - 0x73, 0x74, 0x2d, 0x30, 0x36, 0x2d, 0x32, 0x31, 0x33, 0x38, 0x61, 0x39, - 0x39, 0x31, 0x61, 0xff, - }, - nil, - ).Once(). - On( - "Transmit", - []byte{ - 0x00, 0xa5, 0x00, 0x00, - }). - Return( - []byte{ //nolint:dupl // false-positive - 0x2d, 0x65, 0x63, 0x37, 0x30, 0x2d, 0x34, 0x38, 0x63, 0x62, 0x2d, 0x38, - 0x33, 0x65, 0x36, 0x2d, 0x66, 0x38, 0x30, 0x64, 0x61, 0x34, 0x37, 0x63, - 0x39, 0x33, 0x65, 0x34, 0x72, 0x2d, 0x21, 0x74, 0x65, 0x73, 0x74, 0x2d, - 0x30, 0x37, 0x2d, 0x61, 0x37, 0x30, 0x61, 0x32, 0x35, 0x32, 0x30, 0x2d, - 0x37, 0x65, 0x35, 0x31, 0x2d, 0x34, 0x35, 0x62, 0x32, 0x2d, 0x62, 0x61, - 0x61, 0x62, 0x2d, 0x30, 0x65, 0x33, 0x35, 0x32, 0x32, 0x30, 0x62, 0x30, - 0x36, 0x66, 0x65, 0x72, 0x2d, 0x22, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, - 0x38, 0x2d, 0x38, 0x33, 0x66, 0x65, 0x33, 0x32, 0x30, 0x38, 0x2d, 0x62, - 0x31, 0x39, 0x32, 0x2d, 0x34, 0x36, 0x63, 0x32, 0x2d, 0x39, 0x63, 0x62, - 0x32, 0x2d, 0x31, 0x34, 0x65, 0x65, 0x39, 0x31, 0x37, 0x62, 0x34, 0x64, - 0x36, 0x30, 0x72, 0x2d, 0x23, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x39, - 0x2d, 0x63, 0x63, 0x39, 0x64, 0x31, 0x32, 0x32, 0x65, 0x2d, 0x39, 0x62, - 0x35, 0x31, 0x2d, 0x34, 0x33, 0x35, 0x65, 0x2d, 0x62, 0x34, 0x38, 0x65, - 0x2d, 0x61, 0x62, 0x31, 0x61, 0x31, 0x37, 0x31, 0x35, 0x37, 0x65, 0x33, - 0x63, 0x72, 0x2d, 0x21, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x31, 0x30, 0x2d, - 0x39, 0x37, 0x61, 0x35, 0x38, 0x39, 0x33, 0x38, 0x2d, 0x38, 0x65, 0x61, - 0x36, 0x2d, 0x34, 0x31, 0x34, 0x33, 0x2d, 0x61, 0x65, 0x31, 0x30, 0x2d, - 0x38, 0x61, 0x64, 0x62, 0x39, 0x32, 0x62, 0x64, 0x63, 0x33, 0x33, 0x35, - 0x72, 0x2d, 0x22, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x31, 0x31, 0x2d, 0x38, - 0x38, 0x37, 0x66, 0x64, 0x33, 0x38, 0x62, 0x2d, 0x38, 0x30, 0x62, 0x33, - 0x2d, 0x34, 0x64, 0x37, 0x61, 0x2d, 0x38, 0x36, 0x37, 0x31, 0x2d, 0x38, - 0x32, 0x62, 0x61, 0x38, - }, - nil, - ).Once(). - On( - "Transmit", - []byte{ - 0x00, 0xa5, 0x00, 0x00, - }). - Return( - []byte{ - 0x65, 0x66, 0x36, 0x33, 0x31, 0x35, 0x31, 0x61, 0x36, 0x72, 0x2d, 0x23, - 0x74, 0x65, 0x73, 0x74, 0x2d, 0x31, 0x32, 0x2d, 0x64, 0x61, 0x65, 0x65, - 0x35, 0x30, 0x64, 0x31, 0x2d, 0x37, 0x62, 0x62, 0x66, 0x2d, 0x34, 0x31, - 0x65, 0x36, 0x2d, 0x61, 0x36, 0x35, 0x62, 0x2d, 0x64, 0x33, 0x34, 0x30, - 0x34, 0x36, 0x64, 0x62, 0x61, 0x32, 0x38, 0x37, 0x90, 0x00, - }, - nil, - ).Once() - - client := &OATH{ - card: testCard, - Timestep: DefaultTimeStep, - } - - res, err := client.List() - assert.NoError(err) - assert.Len(res, len(vectors)) - - for idx, r := range res { - name := keys[idx] - - assert.Equal(vectors[name].a, r.Algorithm) - assert.Equal(vectors[name].name, r.Name) - assert.Equal(vectors[name].t, r.Type) - } - - testCard.AssertExpectations(t) + vs := vectorsTOTP[:3] + withCard(t, vs, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + res, err := card.List() + require.NoError(err) + require.Len(res, len(vs)) + + vm := map[string]*vector{} + for _, v := range vs { + v := v + vm[v.Name] = &v + } + + for _, r := range res { + vector, ok := vm[r.Name] + require.True(ok) + + require.Equal(vector.Alg, r.Algorithm) + require.Equal(vector.Name, r.Name) + require.Equal(vector.Typ, r.Type) + } + }) } diff --git a/mockdata/TestCalculate/yk5-5.4.3 b/mockdata/TestCalculate/yk5-5.4.3 new file mode 100644 index 0000000..62af942 --- /dev/null +++ b/mockdata/TestCalculate/yk5-5.4.3 @@ -0,0 +1,17 @@ +# Mockfile/v1 created=2023-11-04T04:21:48+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371089939707db68a16bd9000 +on Transmit 0004dead 9000 +on Transmit 0001000029710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930 9000 +on Transmit 0001000035710f726663363233382d746573742d3032732222083132333435363738393031323334353637383930313233343536373839303132 9000 +on Transmit 0001000055710f726663363233382d746573742d30337342230831323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334 9000 +on Transmit 0001000029710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930 9000 +on Transmit 0001000035710f726663363233382d746573742d3032732222083132333435363738393031323334353637383930313233343536373839303132 9000 +on Transmit 0001000055710f726663363233382d746573742d30337342230831323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334 9000 +on Transmit 00a400010a74080000000000000001 710f726663363233382d746573742d3031760508059eb4ea710f726663363233382d746573742d303276050802bfb94e710f726663363233382d746573742d30337605080567e1309000 +on Transmit 00a400010a74080000000000000001 710f726663363233382d746573742d3031760508059eb4ea710f726663363233382d746573742d303276050802bfb94e710f726663363233382d746573742d30337605080567e1309000 +on Transmit 00a400010a74080000000000000001 710f726663363233382d746573742d3031760508059eb4ea710f726663363233382d746573742d303276050802bfb94e710f726663363233382d746573742d30337605080567e1309000 +on Transmit 00a400010a74080000000000000001 710f726663363233382d746573742d3031760508059eb4ea710f726663363233382d746573742d303276050802bfb94e710f726663363233382d746573742d30337605080567e1309000 +on Transmit 00a400010a74080000000000000001 710f726663363233382d746573742d3031760508059eb4ea710f726663363233382d746573742d303276050802bfb94e710f726663363233382d746573742d30337605080567e1309000 +on Transmit 00a400010a74080000000000000001 710f726663363233382d746573742d3031760508059eb4ea710f726663363233382d746573742d303276050802bfb94e710f726663363233382d746573742d30337605080567e1309000 +on EndTransaction diff --git a/mockdata/TestCalculateHOTPCounterIncrement/yk5-5.4.3 b/mockdata/TestCalculateHOTPCounterIncrement/yk5-5.4.3 new file mode 100644 index 0000000..4b96c72 --- /dev/null +++ b/mockdata/TestCalculateHOTPCounterIncrement/yk5-5.4.3 @@ -0,0 +1,16 @@ +# Mockfile/v1 created=2023-11-04T04:22:05+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371081b2a120cd92b158a9000 +on Transmit 0004dead 9000 +on Transmit 0001000029710f726663343232362d746573742d3030731611063132333435363738393031323334353637383930 9000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 760506000b86189000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 7605060004616a9000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 76050600057af09000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 760506000ecad59000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 7605060005298a9000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 7605060003e2d49000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 760506000464b29000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 76050600027b179000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 760506000619ff9000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 7605060007f1299000 +on EndTransaction diff --git a/mockdata/TestCalculateHOTPCounterInit/yk5-5.4.3 b/mockdata/TestCalculateHOTPCounterInit/yk5-5.4.3 new file mode 100644 index 0000000..8937b5d --- /dev/null +++ b/mockdata/TestCalculateHOTPCounterInit/yk5-5.4.3 @@ -0,0 +1,27 @@ +# Mockfile/v1 created=2023-11-04T04:22:05+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371089f9b25dec7941f729000 +on Transmit 0004dead 9000 +on Transmit 0001000029710f726663343232362d746573742d3030731611063132333435363738393031323334353637383930 9000 +on Transmit 000100002f710f726663343232362d746573742d30317316110631323334353637383930313233343536373839307a0400000001 9000 +on Transmit 000100002f710f726663343232362d746573742d30327316110631323334353637383930313233343536373839307a0400000002 9000 +on Transmit 000100002f710f726663343232362d746573742d30337316110631323334353637383930313233343536373839307a0400000003 9000 +on Transmit 000100002f710f726663343232362d746573742d30347316110631323334353637383930313233343536373839307a0400000004 9000 +on Transmit 000100002f710f726663343232362d746573742d30357316110631323334353637383930313233343536373839307a0400000005 9000 +on Transmit 000100002f710f726663343232362d746573742d30367316110631323334353637383930313233343536373839307a0400000006 9000 +on Transmit 000100002f710f726663343232362d746573742d30377316110631323334353637383930313233343536373839307a0400000007 9000 +on Transmit 000100002f710f726663343232362d746573742d30387316110631323334353637383930313233343536373839307a0400000008 9000 +on Transmit 000100002f710f726663343232362d746573742d30397316110631323334353637383930313233343536373839307a0400000009 9000 +on Transmit 00010000377117726663343232362d386469676974732d746573742d30397316110831323334353637383930313233343536373839307a0400000009 9000 +on Transmit 00a200011b710f726663343232362d746573742d303074080000000000000001 760506000b86189000 +on Transmit 00a200011b710f726663343232362d746573742d303174080000000000000001 7605060004616a9000 +on Transmit 00a200011b710f726663343232362d746573742d303274080000000000000001 76050600057af09000 +on Transmit 00a200011b710f726663343232362d746573742d303374080000000000000001 760506000ecad59000 +on Transmit 00a200011b710f726663343232362d746573742d303474080000000000000001 7605060005298a9000 +on Transmit 00a200011b710f726663343232362d746573742d303574080000000000000001 7605060003e2d49000 +on Transmit 00a200011b710f726663343232362d746573742d303674080000000000000001 760506000464b29000 +on Transmit 00a200011b710f726663343232362d746573742d303774080000000000000001 76050600027b179000 +on Transmit 00a200011b710f726663343232362d746573742d303874080000000000000001 760506000619ff9000 +on Transmit 00a200011b710f726663343232362d746573742d303974080000000000000001 7605060007f1299000 +on Transmit 00a20001237117726663343232362d386469676974732d746573742d303974080000000000000001 76050802b696699000 +on EndTransaction diff --git a/mockdata/TestCalculateMatchFull/yk5-5.4.3 b/mockdata/TestCalculateMatchFull/yk5-5.4.3 new file mode 100644 index 0000000..09b4422 --- /dev/null +++ b/mockdata/TestCalculateMatchFull/yk5-5.4.3 @@ -0,0 +1,7 @@ +# Mockfile/v1 created=2023-11-04T04:21:49+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371084c464d6e70726ef99000 +on Transmit 0004dead 9000 +on Transmit 0001000024710a74657374766563746f72731621083132333435363738393031323334353637383930 9000 +on Transmit 00a400010a74080000000000000001 710a74657374766563746f72760508059eb4ea9000 +on EndTransaction diff --git a/mockdata/TestCalculateMatchMultiple/yk5-5.4.3 b/mockdata/TestCalculateMatchMultiple/yk5-5.4.3 new file mode 100644 index 0000000..e32d4d3 --- /dev/null +++ b/mockdata/TestCalculateMatchMultiple/yk5-5.4.3 @@ -0,0 +1,8 @@ +# Mockfile/v1 created=2023-11-04T04:21:49+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371082d123e89f875201a9000 +on Transmit 0004dead 9000 +on Transmit 0001000025710b74657374766563746f7231731621083132333435363738393031323334353637383930 9000 +on Transmit 0001000025710b74657374766563746f7232731621083132333435363738393031323334353637383930 9000 +on Transmit 00a400010a74080000000000000001 710b74657374766563746f7231760508059eb4ea710b74657374766563746f7232760508059eb4ea9000 +on EndTransaction diff --git a/mockdata/TestCalculateMatchPartial/yk5-5.4.3 b/mockdata/TestCalculateMatchPartial/yk5-5.4.3 new file mode 100644 index 0000000..d161422 --- /dev/null +++ b/mockdata/TestCalculateMatchPartial/yk5-5.4.3 @@ -0,0 +1,7 @@ +# Mockfile/v1 created=2023-11-04T04:21:48+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 79030504037108dbc0fef260e529fc9000 +on Transmit 0004dead 9000 +on Transmit 0001000024710a74657374766563746f72731621083132333435363738393031323334353637383930 9000 +on Transmit 00a400010a74080000000000000001 710a74657374766563746f72760508059eb4ea9000 +on EndTransaction diff --git a/mockdata/TestCalculateRAW/yk5-5.4.3 b/mockdata/TestCalculateRAW/yk5-5.4.3 new file mode 100644 index 0000000..be0b49d --- /dev/null +++ b/mockdata/TestCalculateRAW/yk5-5.4.3 @@ -0,0 +1,7 @@ +# Mockfile/v1 created=2023-11-04T04:22:06+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 7903050403710869e617ed84c7b96a9000 +on Transmit 0004dead 9000 +on Transmit 0001000029710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930 9000 +on Transmit 00a2000018710f726663363233382d746573742d3031740568616c6c6f 75150828c6d33a03e7c67940c30d06253f8980f8ef54bd9000 +on EndTransaction diff --git a/mockdata/TestCalculateRequireTouch/yk5-5.4.3 b/mockdata/TestCalculateRequireTouch/yk5-5.4.3 new file mode 100644 index 0000000..14994ea --- /dev/null +++ b/mockdata/TestCalculateRequireTouch/yk5-5.4.3 @@ -0,0 +1,10 @@ +# Mockfile/v1 created=2023-11-04T04:22:24+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371088c7d2ee45f1771ed9000 +on Transmit 0004dead 9000 +on Transmit 0001000024710e746f7563682072657175697265647310220600000000000000000000123412347802 9000 +on Transmit 00a400010a74080000000000000001 710e746f7563682072657175697265647c01069000 +on Transmit 00a400010a74080000000000000001 710e746f7563682072657175697265647c01069000 +on Transmit 00a400010a74080000000000000001 710e746f7563682072657175697265647c01069000 +on Transmit 00a200011a710e746f75636820726571756972656474080000000000000001 760506000dcc5b9000 +on EndTransaction diff --git a/mockdata/TestCalculateTOTP/yk5-5.4.3 b/mockdata/TestCalculateTOTP/yk5-5.4.3 new file mode 100644 index 0000000..77ff45e --- /dev/null +++ b/mockdata/TestCalculateTOTP/yk5-5.4.3 @@ -0,0 +1,7 @@ +# Mockfile/v1 created=2023-11-04T04:22:04+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 79030504037108548cb7e04b2969a79000 +on Transmit 0004dead 9000 +on Transmit 0001000029710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930 9000 +on Transmit 00a200011b710f726663363233382d746573742d303174080000000000000001 760508059eb4ea9000 +on EndTransaction diff --git a/mockdata/TestDelete/yk5-5.4.3 b/mockdata/TestDelete/yk5-5.4.3 new file mode 100644 index 0000000..4316e43 --- /dev/null +++ b/mockdata/TestDelete/yk5-5.4.3 @@ -0,0 +1,8 @@ +# Mockfile/v1 created=2023-11-04T04:22:06+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 79030504037108be75ebfc9d53719c9000 +on Transmit 0004dead 9000 +on Transmit 0001000029710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930 9000 +on Transmit 0002000011710f726663363233382d746573742d3031 9000 +on Transmit 00a10000 9000 +on EndTransaction diff --git a/mockdata/TestList/yk5-5.4.3 b/mockdata/TestList/yk5-5.4.3 new file mode 100644 index 0000000..f1d9ace --- /dev/null +++ b/mockdata/TestList/yk5-5.4.3 @@ -0,0 +1,9 @@ +# Mockfile/v1 created=2023-11-04T04:44:11+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 79030504037108a4140bf4442462fb9000 +on Transmit 0004dead 9000 +on Transmit 0001000029710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930 9000 +on Transmit 0001000035710f726663363233382d746573742d3032732222083132333435363738393031323334353637383930313233343536373839303132 9000 +on Transmit 0001000055710f726663363233382d746573742d30337342230831323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334 9000 +on Transmit 00a10000 721021726663363233382d746573742d3031721022726663363233382d746573742d3032721023726663363233382d746573742d30339000 +on EndTransaction diff --git a/mockdata/TestPIN/yk5-5.4.3 b/mockdata/TestPIN/yk5-5.4.3 new file mode 100644 index 0000000..895b3ac --- /dev/null +++ b/mockdata/TestPIN/yk5-5.4.3 @@ -0,0 +1,15 @@ +# Mockfile/v1 created=2023-11-04T04:22:07+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 7903050403710880bae14ed6f08caf9000 +on Transmit 0004dead 9000 +on Transmit 00a4040007a0000005272101 790305040371081ac43b4d845166b89000 +on Transmit 00a4040007a0000005272101 790305040371081ac43b4d845166b89000 +on Transmit 00a4040007a0000005272101 790305040371081ac43b4d845166b89000 +on Transmit 000300003f731102497bf21cf91df0c677087a0b779be8df7408225f145cfc487575752027749e37160b8c015b163ac3fe16cf421dbb2336d6c445e0a6ab76d7cec88b52 9000 +on Transmit 00a4040007a0000005272101 790305040371081ac43b4d845166b87408b4ce599512e3e4297b01029000 +on Transmit 00a4040007a0000005272101 790305040371081ac43b4d845166b87408c4e24b29bfb9f9bb7b01029000 +on Transmit 00a300002c7520a3d2ab6e1574e9b50bad39cb4bee69046180cfebe13c929f84eb038ce05a615e74081645b32add642db1 6a80 +on Transmit 00a4040007a0000005272101 790305040371081ac43b4d845166b874085dd49f7561f302eb7b01029000 +on Transmit 00a300002c752085def97420f2bb6dac27bac7c0032c7773a1d3f976b4e1a765e04e1bb095e91074088b797cb2d9682404 7520b4142b7669c7256505cbaa28e2549febfaebf8eadcae19cff1b9907aacfb5ba39000 +on Transmit 00030000027300 9000 +on EndTransaction diff --git a/mockdata/TestPut/yk5-5.4.3 b/mockdata/TestPut/yk5-5.4.3 new file mode 100644 index 0000000..a4b7068 --- /dev/null +++ b/mockdata/TestPut/yk5-5.4.3 @@ -0,0 +1,6 @@ +# Mockfile/v1 created=2023-11-04T04:22:07+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371081ac43b4d845166b89000 +on Transmit 0004dead 9000 +on Transmit 0001000018710474657374731011060000000000000000000000010203 9000 +on EndTransaction diff --git a/mockdata/TestPutNameTooLong/yk5-5.4.3 b/mockdata/TestPutNameTooLong/yk5-5.4.3 new file mode 100644 index 0000000..4109289 --- /dev/null +++ b/mockdata/TestPutNameTooLong/yk5-5.4.3 @@ -0,0 +1,5 @@ +# Mockfile/v1 created=2023-11-04T04:22:08+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371084ee6fc381a0a5dce9000 +on Transmit 0004dead 9000 +on EndTransaction diff --git a/mockdata/TestReset/yk5-5.4.3 b/mockdata/TestReset/yk5-5.4.3 new file mode 100644 index 0000000..df5838a --- /dev/null +++ b/mockdata/TestReset/yk5-5.4.3 @@ -0,0 +1,8 @@ +# Mockfile/v1 created=2023-11-04T04:22:08+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 79030504037108d9f7b0352ed082d99000 +on Transmit 0004dead 9000 +on Transmit 0001000029710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930 9000 +on Transmit 0004dead 9000 +on Transmit 00a10000 9000 +on EndTransaction diff --git a/mockdata/TestSelect/yk5-5.4.3 b/mockdata/TestSelect/yk5-5.4.3 new file mode 100644 index 0000000..ac5d28d --- /dev/null +++ b/mockdata/TestSelect/yk5-5.4.3 @@ -0,0 +1,6 @@ +# Mockfile/v1 created=2023-11-04T04:25:41+01:00 +on BeginTransaction +on Transmit 00a4040007a0000005272101 790305040371088286f7e0dacc2acf9000 +on Transmit 0004dead 9000 +on Transmit 00a4040007a0000005272101 790305040371080d21fb730fcbfdb59000 +on EndTransaction diff --git a/pin.go b/pin.go index 06ccf05..8c00222 100644 --- a/pin.go +++ b/pin.go @@ -6,59 +6,61 @@ package ykoath import ( "bytes" "crypto/hmac" - "crypto/rand" "errors" "fmt" + "cunicu.li/go-iso7816/encoding/tlv" "golang.org/x/crypto/pbkdf2" ) var errTokenResponse = errors.New("invalid token response") -func (o *OATH) RemoveCode() error { - return o.SetCode(nil, HmacSha256) +func (c *Card) RemoveCode() error { + _, err := c.send(insSetCode, 0x00, 0x00, tlv.New(tagKey)) + return err } // SetCode sets a new PIN. // This command no authentication. -func (o *OATH) SetCode(code []byte, alg Algorithm) error { - sel, err := o.Select() +func (c *Card) SetCode(code []byte, alg Algorithm) error { + sel, err := c.Select() if err != nil { return err } key := pbkdf2.Key(code, sel.Name, 1000, 16, alg.Hash()) - myChallenge, err := getChallenge() - if err != nil { - return err + myChallenge := make([]byte, 8) + if _, err := c.Rand.Read(myChallenge); err != nil { + return fmt.Errorf("failed to generate challenge: %w", err) } mac := hmac.New(alg.Hash(), key) mac.Write(myChallenge) myResponse := mac.Sum(nil) - _, err = o.send(0x00, insSetCode, 0x00, 0x00, - write(tagKey, []byte{byte(alg)}, key), - write(tagChallenge, myChallenge), - write(tagResponse, myResponse), - ) + algKey := append([]byte{byte(alg)}, key...) + _, err = c.send(insSetCode, 0x00, 0x00, + tlv.New(tagKey, algKey), + tlv.New(tagChallenge, myChallenge), + tlv.New(tagResponse, myResponse), + ) return err } // Reset resets the application to just-installed state. // This command requires no authentication. -func (o *OATH) Validate(code []byte) error { +func (c *Card) Validate(code []byte) error { var myChallenge, tokenResponse, tokenResponseExpected []byte - sel, err := o.Select() + sel, err := c.Select() if err != nil { return err } if len(sel.Algorithm) < 1 || len(sel.Name) < 1 { - return errNoSuchObject + return ErrNoSuchObject } tokenChallenge := sel.Challenge @@ -70,21 +72,21 @@ func (o *OATH) Validate(code []byte) error { myResponse := mac.Sum(nil) myChallenge = make([]byte, 8) - if _, err := rand.Read(myChallenge); err != nil { + if _, err := c.Rand.Read(myChallenge); err != nil { return fmt.Errorf("failed to generate challenge: %w", err) } - res, err := o.send(0x00, insValidate, 0x00, 0x00, - write(tagResponse, myResponse), - write(tagChallenge, myChallenge), + tvs, err := c.send(insValidate, 0x00, 0x00, + tlv.New(tagResponse, myResponse), + tlv.New(tagChallenge, myChallenge), ) if err != nil { return err } - for _, tv := range res { - if tv.tag == tagResponse { - tokenResponse = tv.value + for _, tv := range tvs { + if tv.Tag == tagResponse { + tokenResponse = tv.Value } } @@ -102,11 +104,3 @@ func (o *OATH) Validate(code []byte) error { return nil } - -func getChallenge() ([]byte, error) { - challenge := make([]byte, 8) - if _, err := rand.Read(challenge); err != nil { - return nil, fmt.Errorf("failed to generate challenge: %w", err) - } - return challenge, nil -} diff --git a/pin_test.go b/pin_test.go index 85bf196..3eab93b 100644 --- a/pin_test.go +++ b/pin_test.go @@ -1,83 +1,60 @@ // SPDX-FileCopyrightText: 2023 Steffen Vogel // SPDX-License-Identifier: Apache-2.0 -//go:build !ci - -package ykoath +package ykoath_test import ( - "flag" "testing" - "github.com/ebfe/scard" "github.com/stretchr/testify/require" -) -// canResetYubikey indicates whether the test running has constented to -// destroying data on YubiKeys connected to the system. -var canResetYubikey = flag.Bool("reset-yubikey", false, - "Flag required to run tests that access the yubikey") + "cunicu.li/go-ykoath" +) func TestPIN(t *testing.T) { - require := require.New(t) - - if !*canResetYubikey { - t.Skip("not running test that accesses yubikey, provide --reset-yubikey flag") - } - - oath, err := New() - require.NoError(err) - - defer oath.Close() - - // Reset token to factory state - _, err = oath.Select() - require.NoError(err) - - err = oath.Reset() - require.NoError(err) - - // Validate should fail if not PIN is set - err = oath.Validate([]byte("1234")) - require.ErrorIs(err, errNoSuchObject) - - // Select applet again - sel, err := oath.Select() - require.NoError(err) - require.Nil(sel.Challenge) - require.Nil(sel.Algorithm) - - // Set PIN - err = oath.SetCode([]byte("1338"), HmacSha256) - require.NoError(err) - - // Reset card to clear authenticated state - card, ok := oath.card.(*scard.Card) - require.True(ok) - - err = card.Reconnect(scard.ShareShared, scard.ProtocolT1, scard.ResetCard) - require.NoError(err) - - // Select applet again - sel, err = oath.Select() - require.NoError(err) - require.NotNil(sel.Challenge) - require.Len(sel.Algorithm, 1) - require.Equal(Algorithm(sel.Algorithm[0]), HmacSha256) - - // RemoveCode should fail as we are not authenticated yet - err = oath.RemoveCode() - require.ErrorIs(err, errAuthRequired) - - // Test invalid PIN - err = oath.Validate([]byte("1337")) - require.ErrorIs(err, errWrongSyntax) - - // Test valid PIN - err = oath.Validate([]byte("1338")) - require.NoError(err) - - // RemoveCode should succeed now - err = oath.RemoveCode() - require.NoError(err) + withCard(t, nil, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + // Validate should fail if not PIN is set + err := card.Validate([]byte("1234")) + require.ErrorIs(err, ykoath.ErrNoSuchObject) + + // Select applet again + sel, err := card.Select() + require.NoError(err) + require.Nil(sel.Challenge) + require.Nil(sel.Algorithm) + + // Set PIN + err = card.SetCode([]byte("1338"), ykoath.HmacSha256) + require.NoError(err) + + // Reset card to clear authenticated state + // err = test.ResetCard(card.Card) + // require.NoError(err) + + // Select applet again + sel, err = card.Select() + require.NoError(err) + + require.NotNil(sel.Challenge) + require.Len(sel.Algorithm, 1) + require.Equal(ykoath.Algorithm(sel.Algorithm[0]), ykoath.HmacSha256) + + // RemoveCode should fail as we are not authenticated yet + // err = card.RemoveCode() + // require.ErrorIs(err, ykoath.ErrAuthRequired) + + // Test invalid PIN + err = card.Validate([]byte("1337")) + require.ErrorIs(err, ykoath.ErrWrongSyntax) + + // Test valid PIN + err = card.Validate([]byte("1338")) + require.NoError(err) + + // RemoveCode should succeed now + err = card.RemoveCode() + require.NoError(err) + }) } diff --git a/put.go b/put.go index 7f13244..dbbd9b6 100644 --- a/put.go +++ b/put.go @@ -4,35 +4,66 @@ package ykoath import ( + "encoding/binary" "errors" "fmt" + + "cunicu.li/go-iso7816/encoding/tlv" ) -var errNameTooLong = errors.New("name too long)") +var ErrNameTooLong = errors.New("name too long)") // Put sends a "PUT" instruction, storing a new / overwriting an existing OATH // credentials with an algorithm and type, 6 or 8 digits one-time password, // shared secrets and touch-required bit -func (o *OATH) Put(name string, a Algorithm, t Type, digits uint8, key []byte, touch bool) error { +func (c *Card) Put(name string, alg Algorithm, typ Type, digits int, key []byte, touch bool, counter uint32) error { if l := len(name); l > 64 { - return fmt.Errorf("%w: (%d > 64)", errNameTooLong, l) + return fmt.Errorf("%w: (%d > 64)", ErrNameTooLong, l) } - var ( - alg = (0xf0|byte(a))&0x0f | byte(t) - dig = digits - prp []byte - ) + key = shortenKey(key, alg) + key = padKey(key) + + tvs := []tlv.TagValue{ + tlv.New(tagName, []byte(name)), + tlv.New(tagKey, []byte{byte(alg) | byte(typ), byte(digits)}, key), + } if touch { - prp = write(tagProperty, []byte{0x02}) + tvs = append(tvs, tlv.TagValue{ + Tag: tagProperty, + Value: []byte{0x02}, + SkipLength: true, + }) } - _, err := o.send(0x00, insPut, 0x00, 0x00, - write(tagName, []byte(name)), - write(tagKey, []byte{alg, dig}, key), - prp, - ) + if counter > 0 { + tvs = append(tvs, tlv.TagValue{ + Tag: tagImf, + Value: binary.BigEndian.AppendUint32(nil, counter), + }) + } + _, err := c.send(insPut, 0x00, 0x00, tvs...) return err } + +func shortenKey(key []byte, alg Algorithm) []byte { + if h := alg.Hash()(); len(key) > h.BlockSize() { + h.Write(key) + return h.Sum(nil) + } + + return key +} + +func padKey(key []byte) []byte { + keyLen := len(key) + if keyLen >= HMACMinimumKeySize { + return key + } + + pad := make([]byte, HMACMinimumKeySize-keyLen) + + return append(pad, key...) +} diff --git a/put_calculate_test.go b/put_calculate_test.go deleted file mode 100644 index f075f22..0000000 --- a/put_calculate_test.go +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Joern Barthel -// SPDX-License-Identifier: Apache-2.0 - -package ykoath - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestPutAndCalculateTestVector(t *testing.T) { - tt := []struct { - Name string - Query string - }{ - { - "full identifier", - "testvector", - }, - { - "name only (substring)", - "test", - }, - } - - for _, test := range tt { - t.Run(test.Name, func(t *testing.T) { - var ( - assert = assert.New(t) - testCard = new(testCard) - ) - - testCard. - On( - "Transmit", - []byte{ - 0x00, 0x01, 0x00, 0x00, 0x24, 0x71, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x76, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x16, 0x21, 0x08, 0x31, 0x32, 0x33, - 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, - 0x36, 0x37, 0x38, 0x39, 0x30, - }). - Return( - []byte{ - 0x90, 0x00, - }, - nil, - ).Once(). - On( - "Transmit", - []byte{ - 0x00, 0xa4, 0x00, 0x01, 0x0a, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, - }). - Return( - []byte{ - 0x71, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x76, 0x05, 0x08, 0x05, 0x9e, 0xb4, 0xea, 0x90, 0x00, - }, - nil, - ).Once() - - client := &OATH{ - card: testCard, - Timestep: DefaultTimeStep, - Clock: func() time.Time { - return time.Unix(59, 0) - }, - } - - err := client.Put("testvector", HmacSha1, Totp, 8, []byte("12345678901234567890"), false) - assert.NoError(err) - - res, err := client.Calculate(test.Query, nil) - assert.NoError(err) - assert.Equal("94287082", res) - - testCard.AssertExpectations(t) - }) - } - - t.Run("multiple match error", func(t *testing.T) { - var ( - assert = assert.New(t) - testCard = new(testCard) - ) - - testCard. - On( - "Transmit", - []byte{ - 0x00, 0x01, 0x00, 0x00, 0x25, 0x71, 0x0b, 0x74, 0x65, 0x73, 0x74, 0x76, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x31, 0x73, 0x16, 0x21, 0x08, 0x31, 0x32, - 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, - 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, - }). - Return( - []byte{ - 0x90, 0x00, - }, - nil, - ).Once(). - On( - "Transmit", - []byte{ - 0x00, 0x01, 0x00, 0x00, 0x25, 0x71, 0x0b, 0x74, 0x65, 0x73, 0x74, 0x76, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x32, 0x73, 0x16, 0x21, 0x08, 0x31, 0x32, - 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, - 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, - }). - Return( - []byte{ - 0x90, 0x00, - }, - nil, - ).Once(). - On( - "Transmit", - []byte{ - 0x00, 0xa4, 0x00, 0x01, 0x0a, 0x74, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, - }). - Return( - []byte{ - 0x71, 0x0b, 0x74, 0x65, 0x73, 0x74, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x31, - 0x76, 0x05, 0x08, 0x05, 0x9e, 0xb4, 0xea, - 0x71, 0x0b, 0x74, 0x65, 0x73, 0x74, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x32, - 0x76, 0x05, 0x08, 0x05, 0x9e, 0xb4, 0xea, - 0x90, 0x00, - }, - nil, - ).Once() - - client := &OATH{ - card: testCard, - Timestep: DefaultTimeStep, - Clock: func() time.Time { - return time.Unix(59, 0) - }, - } - - err := client.Put("testvector1", HmacSha1, Totp, 8, []byte("12345678901234567890"), false) - assert.NoError(err) - - err = client.Put("testvector2", HmacSha1, Totp, 8, []byte("12345678901234567890"), false) - assert.NoError(err) - - _, err = client.Calculate("test", nil) - assert.ErrorIs(err, errMultipleMatches) - - testCard.AssertExpectations(t) - }) -} diff --git a/put_test.go b/put_test.go new file mode 100644 index 0000000..7a473b8 --- /dev/null +++ b/put_test.go @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package ykoath_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cunicu.li/go-ykoath" +) + +func TestPut(t *testing.T) { + withCard(t, nil, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + err := card.Put("test", ykoath.HmacSha1, ykoath.Hotp, 6, []byte{1, 2, 3}, false, 0) + require.NoError(err) + }) +} + +func TestPutNameTooLong(t *testing.T) { + withCard(t, nil, func(t *testing.T, card *ykoath.Card) { + require := require.New(t) + + err := card.Put("0123456789012345678901234567890123456789012345678901234567890123456789", ykoath.HmacSha1, ykoath.Hotp, 6, []byte{1, 2, 3}, false, 0) + require.ErrorIs(err, ykoath.ErrNameTooLong) + }) +} diff --git a/reset.go b/reset.go index 63d40c9..4ac15e5 100644 --- a/reset.go +++ b/reset.go @@ -6,7 +6,7 @@ package ykoath // Reset resets the application to just-installed state. // This command requires no authentication. // WARNING: This function wipes all secrets on the token. Use with care! -func (o *OATH) Reset() error { - _, err := o.send(0x00, insReset, 0xde, 0xad) +func (c *Card) Reset() error { + _, err := c.send(insReset, 0xde, 0xad) return err } diff --git a/reset_test.go b/reset_test.go index 29af9cc..dab7238 100644 --- a/reset_test.go +++ b/reset_test.go @@ -1,40 +1,25 @@ // SPDX-FileCopyrightText: 2018 Joern Barthel // SPDX-License-Identifier: Apache-2.0 -package ykoath +package ykoath_test import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cunicu.li/go-ykoath" ) func TestReset(t *testing.T) { - var ( - assert = assert.New(t) - testCard = new(testCard) - ) - - testCard. - On( - "Transmit", - []byte{ - 0x00, 0x04, 0xde, 0xad, - }). - Return( - []byte{ - 0x90, 0x00, - }, - nil, - ).Once() - - client := &OATH{ - Timestep: DefaultTimeStep, - card: testCard, - } + withCard(t, vectorsTOTP[:1], func(t *testing.T, card *ykoath.Card) { + require := require.New(t) - err := client.Reset() - assert.NoError(err) + err := card.Reset() + require.NoError(err) - testCard.AssertExpectations(t) + creds, err := card.List() + require.NoError(err) + require.Len(creds, 0) + }) } diff --git a/select.go b/select.go index de0f47c..4b26d15 100644 --- a/select.go +++ b/select.go @@ -5,9 +5,10 @@ package ykoath import ( "fmt" -) -var aid = []byte{0xA0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01} //nolint:gochecknoglobals + iso "cunicu.li/go-iso7816" + "cunicu.li/go-iso7816/encoding/tlv" +) // Select encapsulates the results of the "SELECT" instruction type Select struct { @@ -17,33 +18,41 @@ type Select struct { Version []byte } -// Select sends a "SELECT" instruction, initializing the device for an OATH session -func (o *OATH) Select() (*Select, error) { - res, err := o.send(0x00, insSelect, 0x04, 0x00, aid) +func (s *Select) UnmarshalBinary(b []byte) error { + tvs, err := tlv.DecodeSimple(b) if err != nil { - return nil, err + return fmt.Errorf("failed to decode response: %w", err) } - s := new(Select) - - for _, tv := range res { - switch tv.tag { + for _, tv := range tvs { + switch tv.Tag { case tagAlgorithm: - s.Algorithm = tv.value + s.Algorithm = tv.Value case tagChallenge: - s.Challenge = tv.value + s.Challenge = tv.Value case tagName: - s.Name = tv.value + s.Name = tv.Value case tagVersion: - s.Version = tv.value + s.Version = tv.Value default: - return nil, fmt.Errorf("%w (%#x)", errUnknownTag, tv.tag) + return fmt.Errorf("%w (%#x)", errUnknownTag, tv.Tag) } } - return s, nil + return nil +} + +// Select sends a "SELECT" instruction, initializing the device for an OATH session +func (c *Card) Select() (*Select, error) { + resp, err := c.Card.Select(iso.AidYubicoOATH) + if err != nil { + return nil, wrapError(err) + } + + s := &Select{} + return s, s.UnmarshalBinary(resp) } diff --git a/select_test.go b/select_test.go index 9fa2d8b..dac75e8 100644 --- a/select_test.go +++ b/select_test.go @@ -1,48 +1,26 @@ // SPDX-FileCopyrightText: 2018 Joern Barthel // SPDX-License-Identifier: Apache-2.0 -package ykoath +package ykoath_test import ( - "fmt" "testing" "github.com/stretchr/testify/assert" + + "cunicu.li/go-ykoath" ) func TestSelect(t *testing.T) { - var ( - assert = assert.New(t) - testCard = new(testCard) - ) - - testCard. - On( - "Transmit", - []byte{ - 0x00, 0xa4, 0x04, 0x00, 0x07, 0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, - }). - Return( - []byte{ - 0x79, 0x03, 0x04, 0x03, 0x03, 0x71, 0x08, 0x7c, 0x06, 0x60, 0x15, 0x20, - 0xfc, 0x3f, 0x8f, 0x90, 0x00, - }, - nil, - ) - - client := &OATH{ - Timestep: DefaultTimeStep, - card: testCard, - } - - res, err := client.Select() - - assert.Empty(res.Algorithm) - assert.Empty(res.Challenge) - assert.Equal(fmt.Sprintf("% x", []byte{0x7c, 0x06, 0x60, 0x15, 0x20, 0xfc, 0x3f, 0x8f}), fmt.Sprintf("% x", res.Name)) - assert.Equal(fmt.Sprintf("% x", []byte{0x04, 0x03, 0x03}), fmt.Sprintf("% x", res.Version)) + withCard(t, nil, func(t *testing.T, card *ykoath.Card) { + assert := assert.New(t) - assert.NoError(err) + res, err := card.Select() + assert.NoError(err) - testCard.AssertExpectations(t) + assert.Empty(res.Algorithm) + assert.Empty(res.Challenge) + assert.Len(res.Name, 8) // Name gets regenerated during each applet reset + assert.Equal([]byte{0x05, 0x04, 0x03}, res.Version) + }) } diff --git a/tlv.go b/tlv.go deleted file mode 100644 index 50fa9cb..0000000 --- a/tlv.go +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Joern Barthel -// SPDX-License-Identifier: Apache-2.0 - -package ykoath - -import ( - "fmt" -) - -type tv struct { - tag tag - value []byte -} - -type tvs []tv - -// read will read a number of tagged values from a buffer -func read(buf []byte) (tvs tvs) { - var ( - idx int - length int - tagv tag - value []byte - ) - - for { - if len(buf)-idx == 0 { - return tvs - } - - // Read the tag - tagv = tag(buf[idx]) - idx++ - - // Read the length - length = int(buf[idx]) - idx++ - - // Read the value - value = buf[idx : idx+length] - idx += length - - // Append the result - tvs = append(tvs, tv{ - tag: tagv, - value: value, - }) - } -} - -// Write produces a tlv or lv packet (if the tag is 0) -func write(tag tag, values ...[]byte) []byte { - var ( - buf []byte - length int - data []byte - ) - - for _, value := range values { - // Skip nil values (useful for optional tlv segments) - if value == nil { - continue - } - - buf = append(buf, value...) - length += len(value) - } - - // Write the tag unless we skip it (useful for reusing Write for sending the - // APDU) - if tag != 0x00 { - data = append(data, byte(tag)) - } - - // Write some length unless this is a one byte value (e.g. for the PUT - // instruction's "property" byte) - if length > 1 { - data = append(data, byte(length)) - } - - if length > 255 { - panic(fmt.Sprintf("too much data too send (%d bytes)", length)) - } - - return append(data, buf...) -} diff --git a/type.go b/type.go index 9f4be49..9027669 100644 --- a/type.go +++ b/type.go @@ -6,7 +6,6 @@ package ykoath import "fmt" const ( - // Hotp describes HMAC based one-time passwords (https://tools.ietf.org/html/rfc4226) Hotp Type = 0x10 diff --git a/vectors_test.go b/vectors_test.go new file mode 100644 index 0000000..dbd47e4 --- /dev/null +++ b/vectors_test.go @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2018 Joern Barthel +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package ykoath_test + +import ( + "encoding/hex" + "time" + + "cunicu.li/go-ykoath" +) + +type vector struct { + Alg ykoath.Algorithm + Digits int + Secret []byte + Name string + Typ ykoath.Type + Time time.Time + Counter uint32 + Code string + Hash []byte + Touch bool +} + +func fromString(s string) []byte { + return []byte(s) +} + +func fromHex(s string) []byte { + h, err := hex.DecodeString(s) + if err != nil { + panic("failed to parse hex: " + err.Error()) + } + return h +} + +var ( + // See: https://www.rfc-editor.org/errata/eid2866 + testSecretSHA1 = fromString("12345678901234567890") + testSecretSHA256 = fromString("12345678901234567890123456789012") + testSecretSHA512 = fromString("1234567890123456789012345678901234567890123456789012345678901234") + + vectorsTOTP = []vector{ + // RFC 6238 Appendix B - Test Vectors + // See: https://datatracker.ietf.org/doc/html/rfc6238#appendix-B + {Name: "rfc6238-test-01", Alg: ykoath.HmacSha1, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(59, 0), Code: "94287082"}, + {Name: "rfc6238-test-02", Alg: ykoath.HmacSha256, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(59, 0), Code: "46119246"}, + {Name: "rfc6238-test-03", Alg: ykoath.HmacSha512, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(59, 0), Code: "90693936"}, + {Name: "rfc6238-test-04", Alg: ykoath.HmacSha1, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(1111111109, 0), Code: "07081804"}, + {Name: "rfc6238-test-05", Alg: ykoath.HmacSha256, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(1111111109, 0), Code: "68084774"}, + {Name: "rfc6238-test-06", Alg: ykoath.HmacSha512, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(1111111109, 0), Code: "25091201"}, + {Name: "rfc6238-test-07", Alg: ykoath.HmacSha1, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(1111111111, 0), Code: "14050471"}, + {Name: "rfc6238-test-08", Alg: ykoath.HmacSha256, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(1111111111, 0), Code: "67062674"}, + {Name: "rfc6238-test-09", Alg: ykoath.HmacSha512, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(1111111111, 0), Code: "99943326"}, + {Name: "rfc6238-test-10", Alg: ykoath.HmacSha1, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(1234567890, 0), Code: "89005924"}, + {Name: "rfc6238-test-11", Alg: ykoath.HmacSha256, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(1234567890, 0), Code: "91819424"}, + {Name: "rfc6238-test-12", Alg: ykoath.HmacSha512, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(1234567890, 0), Code: "93441116"}, + {Name: "rfc6238-test-13", Alg: ykoath.HmacSha1, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(2000000000, 0), Code: "69279037"}, + {Name: "rfc6238-test-14", Alg: ykoath.HmacSha256, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(2000000000, 0), Code: "90698825"}, + {Name: "rfc6238-test-15", Alg: ykoath.HmacSha512, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(2000000000, 0), Code: "38618901"}, + {Name: "rfc6238-test-16", Alg: ykoath.HmacSha1, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(20000000000, 0), Code: "65353130"}, + {Name: "rfc6238-test-17", Alg: ykoath.HmacSha256, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(20000000000, 0), Code: "77737706"}, + {Name: "rfc6238-test-18", Alg: ykoath.HmacSha512, Typ: ykoath.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(20000000000, 0), Code: "47863826"}, + + {Name: "rfc6238-6digits-test-18", Alg: ykoath.HmacSha512, Typ: ykoath.Totp, Digits: 6, Secret: testSecretSHA512, Time: time.Unix(20000000000, 0), Code: "863826"}, + } + + vectorsHOTP = []vector{ + // RFC 4226 Appendix D - HOTP Algorithm: Test Values + // See: https://datatracker.ietf.org/doc/html/rfc4226#page-32 + {Name: "rfc4226-test-00", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 0, Code: "755224", Hash: fromHex("cc93cf18508d94934c64b65d8ba7667fb7cde4b0")}, + {Name: "rfc4226-test-01", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 1, Code: "287082", Hash: fromHex("75a48a19d4cbe100644e8ac1397eea747a2d33ab")}, + {Name: "rfc4226-test-02", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 2, Code: "359152", Hash: fromHex("0bacb7fa082fef30782211938bc1c5e70416ff44")}, + {Name: "rfc4226-test-03", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 3, Code: "969429", Hash: fromHex("66c28227d03a2d5529262ff016a1e6ef76557ece")}, + {Name: "rfc4226-test-04", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 4, Code: "338314", Hash: fromHex("a904c900a64b35909874b33e61c5938a8e15ed1c")}, + {Name: "rfc4226-test-05", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 5, Code: "254676", Hash: fromHex("a37e783d7b7233c083d4f62926c7a25f238d0316")}, + {Name: "rfc4226-test-06", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 6, Code: "287922", Hash: fromHex("bc9cd28561042c83f219324d3c607256c03272ae")}, + {Name: "rfc4226-test-07", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 7, Code: "162583", Hash: fromHex("a4fb960c0bc06e1eabb804e5b397cdc4b45596fa")}, + {Name: "rfc4226-test-08", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 8, Code: "399871", Hash: fromHex("1b3c89f65e6c9e883012052823443f048b4332db")}, + {Name: "rfc4226-test-09", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 6, Secret: testSecretSHA1, Counter: 9, Code: "520489", Hash: fromHex("1637409809a679dc698207310c8c7fc07290d9e5")}, + + {Name: "rfc4226-8digits-test-09", Alg: ykoath.HmacSha1, Typ: ykoath.Hotp, Digits: 8, Secret: testSecretSHA1, Counter: 9, Code: "45520489", Hash: fromHex("1637409809a679dc698207310c8c7fc07290d9e5")}, + } + + vectors = map[string][]vector{ + "TOTP": vectorsTOTP, + "HOTP": vectorsHOTP, + } +) diff --git a/ykoath.go b/ykoath.go index 5b0498a..ec208df 100644 --- a/ykoath.go +++ b/ykoath.go @@ -4,12 +4,14 @@ package ykoath import ( + "crypto/rand" "errors" "fmt" - "strings" + "io" "time" - "github.com/ebfe/scard" + iso "cunicu.li/go-iso7816" + "cunicu.li/go-iso7816/encoding/tlv" ) const ( @@ -17,160 +19,101 @@ const ( HMACMinimumKeySize = 14 ) -type ( - tag byte - instruction byte -) - // TLV tags for credential data const ( - tagName tag = 0x71 - tagNameList tag = 0x72 - tagKey tag = 0x73 - tagChallenge tag = 0x74 - tagResponse tag = 0x75 - tagTruncated tag = 0x76 - tagHOTP tag = 0x77 - tagProperty tag = 0x78 - tagVersion tag = 0x79 - tagImf tag = 0x7A - tagAlgorithm tag = 0x7B - tagTouch tag = 0x7C + tagName tlv.Tag = 0x71 + tagNameList tlv.Tag = 0x72 + tagKey tlv.Tag = 0x73 + tagChallenge tlv.Tag = 0x74 + tagResponse tlv.Tag = 0x75 + tagTruncated tlv.Tag = 0x76 + tagHOTP tlv.Tag = 0x77 + tagProperty tlv.Tag = 0x78 + tagVersion tlv.Tag = 0x79 + tagImf tlv.Tag = 0x7A + tagAlgorithm tlv.Tag = 0x7B + tagTouch tlv.Tag = 0x7C ) // Instruction bytes for commands const ( - insList instruction = 0xA1 - insSelect instruction = 0xA4 - insPut instruction = 0x01 - insDelete instruction = 0x02 - insSetCode instruction = 0x03 - insReset instruction = 0x04 - insRename instruction = 0x05 - insCalculate instruction = 0xA2 - insValidate instruction = 0xA3 - insCalculateAll instruction = 0xA4 - insSendRemaining instruction = 0xA5 + insList iso.Instruction = 0xA1 + insSelect iso.Instruction = 0xA4 + insPut iso.Instruction = 0x01 + insDelete iso.Instruction = 0x02 + insSetCode iso.Instruction = 0x03 + insReset iso.Instruction = 0x04 + insRename iso.Instruction = 0x05 + insCalculate iso.Instruction = 0xA2 + insValidate iso.Instruction = 0xA3 + insCalculateAll iso.Instruction = 0xA4 + insSendRemaining iso.Instruction = 0xA5 ) -type card interface { - Disconnect(scard.Disposition) error - Transmit([]byte) ([]byte, error) -} - -type context interface { - Release() error -} +// Card implements most parts of the TOTP portion of the YKOATH specification +// https://developers.yubico.com/Card/YKOATH_Protocol.html +type Card struct { + *iso.Card -// OATH implements most parts of the TOTP portion of the YKOATH specification -// https://developers.yubico.com/OATH/YKOATH_Protocol.html -type OATH struct { Clock func() time.Time Timestep time.Duration + Rand io.Reader - card card - context context + tx *iso.Transaction } -var ( - errFailedToConnect = errors.New("failed to connect to reader") - errFailedToDisconnect = errors.New("failed to disconnect from reader") - errFailedToEstablishContext = errors.New("failed to establish context") - errFailedToListReaders = errors.New("failed to list readers") - errFailedToListSuitableReader = errors.New("no suitable reader found") - errFailedToReleaseContext = errors.New("failed to release context") - errFailedToTransmit = errors.New("failed to transmit APDU") - errUnknownTag = errors.New("unknown tag") -) +var errUnknownTag = errors.New("unknown tag") -// New initializes a new OATH session -func New() (*OATH, error) { - context, err := scard.EstablishContext() - if err != nil { - return nil, fmt.Errorf("%w: %w", errFailedToEstablishContext, err) - } +// NewCard initializes a new OATH card. +func NewCard(pcscCard iso.PCSCCard) (*Card, error) { + isoCard := iso.NewCard(pcscCard) + isoCard.InsGetRemaining = insSendRemaining - readers, err := context.ListReaders() + tx, err := isoCard.NewTransaction() if err != nil { - return nil, fmt.Errorf("%w: %w", errFailedToListReaders, err) + return nil, fmt.Errorf("failed to initiate transaction: %w", err) } - for _, reader := range readers { - if isYkoathToken(reader) { - card, err := context.Connect(reader, scard.ShareShared, scard.ProtocolAny) - if err != nil { - return nil, fmt.Errorf("%w: %w", errFailedToConnect, err) - } - - return &OATH{ - Clock: time.Now, - Timestep: DefaultTimeStep, - card: card, - context: context, - }, nil - } - } + return &Card{ + Card: isoCard, + Clock: time.Now, + Timestep: DefaultTimeStep, + Rand: rand.Reader, - return nil, fmt.Errorf("%w (out of %d)", errFailedToListSuitableReader, len(readers)) + tx: tx, + }, nil } // Close terminates an OATH session -func (o *OATH) Close() error { - if err := o.card.Disconnect(scard.LeaveCard); err != nil { - return fmt.Errorf("%w: %w", errFailedToDisconnect, err) - } - - if err := o.context.Release(); err != nil { - return fmt.Errorf("%w: %w", errFailedToReleaseContext, err) +func (c *Card) Close() error { + if c.tx != nil { + if err := c.tx.EndTransaction(); err != nil { + return err + } } return nil } -// send sends an APDU to the card -// nolint: unparam -func (o *OATH) send(cla byte, ins instruction, p1, p2 byte, data ...[]byte) (tvs, error) { - var ( - rcode code - results []byte - send = append([]byte{cla, byte(ins), p1, p2}, write(0x00, data...)...) - ) - - for { - res, err := o.card.Transmit(send) - if err != nil { - return nil, fmt.Errorf("%w: %w", errFailedToTransmit, err) - } - - rcode = code(res[len(res)-2:]) - results = append(results, res[0:len(res)-2]...) - - switch { - case rcode.IsMore(): - send = []byte{0x00, byte(insSendRemaining), 0x00, 0x00} - - case rcode == errSuccess: - return read(results), nil - - default: - return nil, rcode - } +func (c *Card) send(ins iso.Instruction, p1, p2 byte, tvsCmd ...tlv.TagValue) (tvsResp []tlv.TagValue, err error) { + data, err := tlv.EncodeSimple(tvsCmd...) + if err != nil { + return nil, fmt.Errorf("failed to encode command: %w", err) } -} -func isYkoathToken(token string) bool { - compatibleTokens := []string{ - "yubikey", - "nitrokey", + res, err := c.Card.Send(&iso.CAPDU{ + Ins: ins, + P1: p1, + P2: p2, + Data: data, + }) + if err != nil { + return nil, wrapError(err) } - token = strings.ToLower(token) - - for _, comcompatibleToken := range compatibleTokens { - if strings.Contains(token, comcompatibleToken) { - return true - } + if tvsResp, err = tlv.DecodeSimple(res); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) } - return false + return tvsResp, nil } diff --git a/ykoath_test.go b/ykoath_test.go index c50abe8..d0e84a5 100644 --- a/ykoath_test.go +++ b/ykoath_test.go @@ -1,161 +1,52 @@ -// SPDX-FileCopyrightText: 2018 Joern Barthel +// SPDX-FileCopyrightText: 2023 Steffen Vogel // SPDX-License-Identifier: Apache-2.0 -package ykoath +package ykoath_test import ( - "sort" + "math/rand" + "testing" + "time" - "github.com/ebfe/scard" - "github.com/stretchr/testify/mock" + iso "cunicu.li/go-iso7816" + yk "cunicu.li/go-iso7816/devices/yubikey" + "cunicu.li/go-iso7816/test" + "github.com/stretchr/testify/require" + + "cunicu.li/go-ykoath" ) -type testCard struct { - mock.Mock -} +// withCard is a helper to initialize a card for testing +func withCard(t *testing.T, vs []vector, cb func(t *testing.T, card *ykoath.Card)) { + test.WithCard(t, yk.HasOATH, func(t *testing.T, isoCard *iso.Card) { + require := require.New(t) -func (t *testCard) Disconnect(d scard.Disposition) error { - args := t.Called(d) - return args.Error(0) -} + oathCard, err := ykoath.NewCard(isoCard) + require.NoError(err) -func (t *testCard) Transmit(b []byte) ([]byte, error) { - args := t.Called(b) - return args.Get(0).([]byte), args.Error(1) //nolint:forcetypeassert -} + _, err = oathCard.Select() + require.NoError(err, "Failed to select applet") -type vector struct { - a Algorithm - digits uint8 - key []byte - name string - t Type - testvector string - time int64 - touch bool -} + err = oathCard.Reset() + require.NoError(err, "Failed to reset applet") -var ( - keys sort.StringSlice - vectors map[string]*vector -) + for _, v := range vs { + v := v + err = oathCard.Put(v.Name, v.Alg, v.Typ, v.Digits, v.Secret, v.Touch, v.Counter) + require.NoError(err, "Failed to put credential") + } + + // Fix the clock for our tests + oathCard.Clock = func() time.Time { + return time.Unix(59, 0) + } -func init() { //nolint:gochecknoinits - vectors = map[string]*vector{ - "test-01-1e5f2db9-477e-41af-bd2e-60bc569ae871": { - a: HmacSha1, - t: Totp, - digits: 6, - key: []byte("12345678901234567890"), - touch: false, - time: 59, - testvector: "287082", - }, - "test-02-2a7cbca9-baef-47e3-8ce8-788bc6853e12": { - a: HmacSha256, - t: Totp, - digits: 6, - key: []byte("12345678901234567890123456789012"), - touch: true, - time: 59, - testvector: "119246", - }, - "test-03-b01019ed-2af1-48cc-a64c-fa9b424db993": { - a: HmacSha512, - t: Totp, - digits: 6, - key: []byte("1234567890123456789012345678901234567890123456789012345678901234"), - touch: false, - time: 59, - testvector: "693936", - }, - "test-04-e62171f0-4cf6-499e-b988-6ef36b213cc6": { - a: HmacSha1, - t: Totp, - digits: 6, - key: []byte("12345678901234567890"), - touch: true, - time: 59, - testvector: "287082", - }, - "test-05-458af9ee-caaa-4716-bfb8-bd828757955d": { - a: HmacSha256, - t: Totp, - digits: 6, - key: []byte("12345678901234567890123456789012"), - touch: false, - time: 59, - testvector: "119246", - }, - "test-06-2138a991-ec70-48cb-83e6-f80da47c93e4": { - a: HmacSha512, - t: Totp, - digits: 6, - key: []byte("1234567890123456789012345678901234567890123456789012345678901234"), - touch: true, - time: 59, - testvector: "693936", - }, - "test-07-a70a2520-7e51-45b2-baab-0e35220b06fe": { - a: HmacSha1, - t: Totp, - digits: 8, - key: []byte("12345678901234567890"), - touch: false, - time: 59, - testvector: "94287082", - }, - "test-08-83fe3208-b192-46c2-9cb2-14ee917b4d60": { - a: HmacSha256, - t: Totp, - digits: 8, - key: []byte("12345678901234567890123456789012"), - touch: true, - time: 59, - testvector: "46119246", - }, - "test-09-cc9d122e-9b51-435e-b48e-ab1a17157e3c": { - a: HmacSha512, - t: Totp, - digits: 8, - key: []byte("1234567890123456789012345678901234567890123456789012345678901234"), - touch: false, - time: 59, - testvector: "90693936", - }, - "test-10-97a58938-8ea6-4143-ae10-8adb92bdc335": { - a: HmacSha1, - t: Totp, - digits: 8, - key: []byte("12345678901234567890"), - touch: true, - time: 59, - testvector: "94287082", - }, - "test-11-887fd38b-80b3-4d7a-8671-82bef63151a6": { - a: HmacSha256, - t: Totp, - digits: 8, - key: []byte("12345678901234567890123456789012"), - touch: false, - time: 59, - testvector: "46119246", - }, - "test-12-daee50d1-7bbf-41e6-a65b-d34046dba287": { - a: HmacSha512, - t: Totp, - digits: 8, - key: []byte("1234567890123456789012345678901234567890123456789012345678901234"), - touch: true, - time: 59, - testvector: "90693936", - }, - } + // Fix the random source for reproducible tests + oathCard.Rand = rand.New(rand.NewSource(4242)) //nolint:gosec - for k, v := range vectors { - keys = append(keys, k) - v.name = k - } + cb(t, oathCard) - keys.Sort() + err = oathCard.Close() + require.NoError(err) + }) }