diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5f84648..23f3a06 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 Steffen Vogel +# SPDX-FileCopyrightText: 2024 Steffen Vogel # SPDX-License-Identifier: Apache-2.0 # yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/github-workflow.json @@ -14,9 +14,9 @@ on: jobs: build: name: Build - + runs-on: ${{ matrix.os }} - + strategy: fail-fast: false matrix: diff --git a/.golangci.yaml b/.golangci.yaml index 1a37ee0..9bfa845 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 Steffen Vogel +# SPDX-FileCopyrightText: 2024 Steffen Vogel # SPDX-License-Identifier: Apache-2.0 linters-settings: @@ -27,7 +27,7 @@ linters-settings: sections: - standard - default - - prefix(cunicu.li/skeleton) + - prefix(cunicu.li/go-trussed-secrets) - blank - dot diff --git a/.reuse/dep5 b/.reuse/dep5 index 88d4469..2af2ad2 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,7 +1,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: skeleton +Upstream-Name: go-trussed-secrets Upstream-Contact: Steffen Vogel -Source: https://github.com/stv0g/skeleton +Source: https://github.com/stv0g/go-trussed-secrets Files: go.sum .renovaterc.json flake.lock Copyright: 2023 Steffen Vogel diff --git a/CODEOWNERS b/CODEOWNERS index 4720a19..0c121ac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 Steffen Vogel +# SPDX-FileCopyrightText: 2024 Steffen Vogel # SPDX-License-Identifier: Apache-2.0 * @stv0g diff --git a/README.md b/README.md index 0908f05..bf36caf 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,65 @@ -# skeleton: Template repository for Go packages in the cunicu organization +# go-trussed-secrets -[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/cunicu/skeleton/test.yaml?style=flat-square)](https://github.com/cunicu/skeleton/actions) -[![goreportcard](https://goreportcard.com/badge/github.com/cunicu/skeleton?style=flat-square)](https://goreportcard.com/report/github.com/cunicu/skeleton) -[![Codecov branch](https://img.shields.io/codecov/c/github/cunicu/skeleton/main?style=flat-square&token=6XoWouQg6K)](https://app.codecov.io/gh/cunicu/skeleton/tree/main) -[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/cunicu/skeleton/blob/main/LICENSES/Apache-2.0.txt) -![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/cunicu/skeleton?style=flat-square) -[![Go Reference](https://pkg.go.dev/badge/github.com/cunicu/skeleton.svg)](https://pkg.go.dev/github.com/cunicu/skeleton) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/cunicu/go-trussed-secrets/test.yaml?style=flat-square)](https://github.com/cunicu/go-trussed-secrets/actions) +[![goreportcard](https://goreportcard.com/badge/github.com/cunicu/go-trussed-secrets?style=flat-square)](https://goreportcard.com/report/github.com/cunicu/go-trussed-secrets) +[![Codecov branch](https://img.shields.io/codecov/c/github/cunicu/go-trussed-secrets/main?style=flat-square&token=6XoWouQg6K)](https://app.codecov.io/gh/cunicu/go-trussed-secrets/tree/main) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/cunicu/go-trussed-secrets/blob/main/LICENSES/Apache-2.0.txt) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/cunicu/go-trussed-secrets?style=flat-square) +[![Go Reference](https://pkg.go.dev/badge/github.com/cunicu/go-trussed-secrets.svg)](https://pkg.go.dev/github.com/cunicu/go-trussed-secrets) -This `skeleton` repo a template for new Go packages belonging to the cunicu organization. +The package `go-trussed-secrets` implements the protocol of the [_Trussed Secrets App_](https://github.com/Nitrokey/trussed-secrets-app). +It is used by the Nitrokey 3 tokens to provide HOTP, TOTP, reverse HOTP and challenge/response credentials. -## Install +**Note:** This package uses the CCID smart-card interface rather than the [CTAPHID](https://github.com/Nitrokey/trussed-secrets-app/blob/main/docs/ctaphid.md) interface as used by [pynitrokey](https://github.com/Nitrokey/pynitrokey) -## Usage +## Features + +- Calculation of + - Time-based One-time Passwords (TOTP) + - Hash-based One-time Passwords (HOTP) + - Reverse Hash-based One-time Passwords (HOTP) + - Static password safe entries +- PIN based authentication +- Factory reset of applet +- Credential management + - Add + - Get + - List + - Remove + - Update + - Rename + +### Unimplemented + +The following features have not been implemented as they have been deprecated in [Nitrokey/trussed-secrets-app](https://github.com/Nitrokey/trussed-secrets-app): + +- [YubiKey challenge/response slots](https://docs.yubico.com/yesdk/users-manual/application-otp/challenge-response.html) (as used by KeePassXC) + - Also includes challenge/response-based PIN authentication +- `CalculateAll` instruction + - Is disabled by default on Nitrokey 3's + +## Roadmap + +- [Untruncated responses](https://github.com/Nitrokey/trussed-secrets-app/issues/116) +- [CTAPHID interface](https://github.com/Nitrokey/trussed-secrets-app/blob/main/docs/ctaphid.md) + +## Tested devices + +- Nitrokey 3 + - FW version v1.7.0 + +## References + +- [**RFC 4226:** HOTP: An HMAC-Based One-Time Password Algorithm](https://datatracker.ietf.org/doc/html/rfc4226) +- [**RFC 6238:** TOTP: Time-Based One-Time Password Algorithm](https://datatracker.ietf.org/doc/html/rfc6238) +- [CTAPHID Protocol specification](https://github.com/Nitrokey/trussed-secrets-app/blob/main/docs/ctaphid.md) +- Token App: [Nitrokey/trussed-secrets-app](https://github.com/Nitrokey/trussed-secrets-app) +- Client CLI: [Nitrokey/pynitrokey (`pynitrokey/nk3/secrets_app.py`)](https://github.com/Nitrokey/pynitrokey/blob/master/pynitrokey/nk3/secrets_app.py) ## Authors @@ -24,4 +67,4 @@ This `skeleton` repo a template for new Go packages belonging to the cunicu orga ## License -skeleton is licensed under the [Apache 2.0](./LICENSE) license. +go-trussed-secrets is licensed under the [Apache 2.0](./LICENSE) license. diff --git a/algorithm.go b/algorithm.go new file mode 100644 index 0000000..fd1f05a --- /dev/null +++ b/algorithm.go @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "crypto/sha1" //nolint:gosec + "crypto/sha256" + "crypto/sha512" + "fmt" + "hash" +) + +// Algorithm denotes the HMAC algorithm used for deriving the one-time passwords +type Algorithm byte + +const ( + // HMACSHA1 describes a HMAC with SHA-1. + HMACSHA1 Algorithm = 0x01 + + // HMACSHA256 describes a HMAC with SHA-2 (256-bit). + HMACSHA256 Algorithm = 0x02 + + // HMACSHA512 describes a HMAC with SHA-2 (512-bit). + // TODO: Not yet supported by firmware. + HMACSHA512 Algorithm = 0x03 +) + +// String returns a string representation of the algorithm. +func (a Algorithm) String() string { + switch a { + case HMACSHA1: + return "HMAC-SHA1" + + case HMACSHA256: + return "HMAC-SHA256" + + case HMACSHA512: + return "HMAC-SHA512" + + default: + return fmt.Sprintf("unknown %x", byte(a)) + } +} + +// Hash returns a constructor to create a new hash.Hash object +// for the given algorithm. +func (a Algorithm) Hash() func() hash.Hash { + switch a { + case HMACSHA1: + return sha1.New + + case HMACSHA256: + return sha256.New + + case HMACSHA512: + return sha512.New + + default: + return nil + } +} diff --git a/calculate.go b/calculate.go new file mode 100644 index 0000000..0476fa0 --- /dev/null +++ b/calculate.go @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "encoding/binary" + "errors" + "fmt" + "time" + + "cunicu.li/go-iso7816/encoding/tlv" +) + +var ErrNoValuesFound = errors.New("no values found in response") + +// Calculate calculates a TOTP, HOTP with using the clock or current counter value. +func (c *Card) Calculate(id string) (Code, error) { + return c.CalculateWithChallenge(id, ChallengeTOTP(c.Clock(), c.Timestep)) +} + +// Calculate a TOTP or HMAC by providing a challenge. +func (c *Card) CalculateWithChallenge(id string, challenge []byte) (Code, error) { + // Unlike in YKOATH, the Trussed secrets app only returns truncated + // codes for HOTP and TOTP credentials. + tvs, err := c.send(insCalculate, 0x00, 0x01, + tlv.New(tagCredentialID, []byte(id)), + tlv.New(tagChallenge, challenge), + ) + if err != nil { + return Code{}, err + } + + for _, tv := range tvs { + switch tv.Tag { + case tagResponse, tagTruncated: + digits := int(tv.Value[0]) + hash := tv.Value[1:] + return Code{ + Digest: hash, + Digits: digits, + Truncated: tv.Tag == tagTruncated, + }, nil + + default: + return Code{}, fmt.Errorf("%w: %x", errUnknownTag, tv.Tag) + } + } + + return Code{}, ErrNoValuesFound +} + +func ChallengeTOTP(t time.Time, ts time.Duration) []byte { + counter := t.Unix() / int64(ts.Seconds()) + return binary.BigEndian.AppendUint64(nil, uint64(counter)) +} diff --git a/calculate_test.go b/calculate_test.go new file mode 100644 index 0000000..72d982e --- /dev/null +++ b/calculate_test.go @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestCalculate(t *testing.T) { + vs := vectorsTOTP + vs = append(vs, vectorsHOTP...) + + withCard(t, vs, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + for _, v := range vs { + chal := secrets.ChallengeTOTP(v.Time, secrets.DefaultTimeStep) + code, err := card.CalculateWithChallenge(v.ID, chal) + require.NoError(err) + require.Equal(v.Code, code.OTP()) + } + }) +} + +// func TestCalculateRequireTouch(t *testing.T) { +// withCard(t, []vector{ +// { +// ID: "touch-required", +// Algorithm: secrets.HMACSHA256, +// Kind: secrets.TOTP, +// Digits: 6, +// Secret: fromHex("12341234"), +// Properties: secrets.TouchRequired, +// }, +// }, true, func(t *testing.T, card *secrets.Card) { +// require := require.New(t) + +// // Callback missing +// _, err := card.Calculate("touch-required") +// require.ErrorIs(err, secrets.ErrTouchCallbackRequired) + +// // Error raised in callback +// _, err = card.Calculate("touch-required", 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-required", func(s string) error { +// require.Equal(s, "touch-required") +// touchRequested = true +// return nil +// }) +// require.NoError(err) +// require.True(touchRequested) +// }) +// } + +func TestCalculateTOTP(t *testing.T) { + v := vectorsTOTP[0] + withCard(t, []vector{v}, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + code, err := card.Calculate(v.ID) + require.NoError(err) + require.Equal(v.Code, code) + }) +} + +func TestCalculateHOTPCounterIncrement(t *testing.T) { + v := vectorsHOTP[0] + withCard(t, []vector{v}, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + for _, ev := range vectorsHOTP[:10] { + code, err := card.Calculate(v.ID) + require.NoError(err) + require.Equal(ev.Code, code.OTP()) + require.Equal(v.Digits, code.Digits) + require.True(code.Truncated) + } + }) +} + +func TestCalculateHOTPCounterInit(t *testing.T) { + withCard(t, vectorsHOTP, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + for _, v := range vectorsHOTP { + code, err := card.Calculate(v.ID) + require.NoError(err) + require.Equal(v.Code, code.OTP()) + require.Equal(v.Digits, code.Digits) + require.True(code.Truncated) + } + }) +} + +// func TestCalculateHMAC(t *testing.T) { +// expResp := fromHex("28c6d33a03e7c67940c30d06253f8980f8ef54bd") + +// v := vector{ +// ID: "hmac-test-01", +// Algorithm: secrets.HMACSHA1, +// Kind: secrets.HMAC, +// Secret: testSecretSHA1, +// } + +// withCard(t, []vector{v}, true, func(t *testing.T, card *secrets.Card) { +// require := require.New(t) + +// code, err := card.CalculateWithChallenge(v.ID, fromString("hallo")) +// require.NoError(err) +// require.Equal(expResp, code.Hash) +// require.False(code.Truncated) +// require.Zero(code.Digits) + +// hash := v.Algorithm.Hash()() +// require.Len(code.Hash, hash.Size()) +// }) +// } diff --git a/card.go b/card.go new file mode 100644 index 0000000..0dd9905 --- /dev/null +++ b/card.go @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "crypto/rand" + "errors" + "fmt" + "io" + "time" + + iso "cunicu.li/go-iso7816" + "cunicu.li/go-iso7816/encoding/tlv" +) + +const ( + DefaultTimeStep = 30 * time.Second + HMACMinimumKeySize = 14 +) + +// TLV tags for credential data. +// +// See: https://github.com/Nitrokey/pynitrokey/blob/3d2495155dd3c56c02625e362a3106d96ca75286/pynitrokey/nk3/secrets_app.py#L226 +const ( + tagCredentialID tlv.Tag = 0x71 // Also known as Name or Label + tagNameList tlv.Tag = 0x72 + tagKey tlv.Tag = 0x73 + tagChallenge tlv.Tag = 0x74 + tagResponse tlv.Tag = 0x75 + tagTruncated tlv.Tag = 0x76 + tagProperties tlv.Tag = 0x78 + tagInitialCounter tlv.Tag = 0x7A + tagVersion tlv.Tag = 0x79 + tagAlgorithm tlv.Tag = 0x7B + + // Extensions starting from 0x80 + tagPassword tlv.Tag = 0x80 + tagNewPassword tlv.Tag = 0x81 + tagPINCounter tlv.Tag = 0x82 + tagPWSLogin tlv.Tag = 0x83 + tagPWSPassword tlv.Tag = 0x84 + tagPWSMetadata tlv.Tag = 0x85 + tagSerialNumber tlv.Tag = 0x8F +) + +// Instruction bytes for commands. +// +// See: https://github.com/Nitrokey/pynitrokey/blob/3d2495155dd3c56c02625e362a3106d96ca75286/pynitrokey/nk3/secrets_app.py#L207 +const ( + insPut iso.Instruction = 0x01 // Register a new OTP credential + insDelete iso.Instruction = 0x02 // Register a new OTP credential + + insReset iso.Instruction = 0x04 // Remove all stored OTP credentials + + insList iso.Instruction = 0xA1 // List stored OTP credentials + insCalculate iso.Instruction = 0xA2 // Calculate an OTP code for the credential + insValidate iso.Instruction = 0xA3 // + + insSendRemaining iso.Instruction = 0xA5 + + // Place extending commands in 0xBx space + insVerifyCode iso.Instruction = 0xB1 // Reverse HOTP - verify incoming HOTP code + + insVerifyPIN iso.Instruction = 0xB2 // Authenticate with provided PIN + insChangePIN iso.Instruction = 0xB3 // Change PIN + insSetPIN iso.Instruction = 0xB4 // Set PIN. Can be called only once, directly after factory reset. + + insGetCredential iso.Instruction = 0xB5 // Get static password entry + insUpdateCredential iso.Instruction = 0xB7 // Update static password entry +) + +type Card struct { + *iso.Card + + Clock func() time.Time + Timestep time.Duration + Rand io.Reader + + info *Select + tx *iso.Transaction +} + +var errUnknownTag = errors.New("unknown tag") + +// NewCard initializes a new card. +func NewCard(pcscCard iso.PCSCCard) (*Card, error) { + isoCard := iso.NewCard(pcscCard) + isoCard.InsGetRemaining = insSendRemaining + + tx, err := isoCard.NewTransaction() + if err != nil { + return nil, fmt.Errorf("failed to initiate transaction: %w", err) + } + + return &Card{ + Card: isoCard, + Clock: time.Now, + Timestep: DefaultTimeStep, + Rand: rand.Reader, + + tx: tx, + }, nil +} + +// Close terminates the session. +func (c *Card) Close() error { + if c.tx != nil { + if err := c.tx.EndTransaction(); err != nil { + return err + } + } + + return nil +} + +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) + } + + return c.sendRaw(ins, p1, p2, data) +} + +func (c *Card) sendRaw(ins iso.Instruction, p1, p2 byte, data []byte) (tvsResp []tlv.TagValue, err error) { + cmd := &iso.CAPDU{ + Ins: ins, + P1: p1, + P2: p2, + Data: data, + Ne: iso.MaxLenRespDataStandard, + } + + res, err := c.tx.Send(cmd) + if err != nil { + return nil, wrapError(err) + } + + if tvsResp, err = tlv.DecodeSimple(res); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return tvsResp, nil +} diff --git a/card_test.go b/card_test.go new file mode 100644 index 0000000..4c99dcf --- /dev/null +++ b/card_test.go @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "math/rand" + "testing" + "time" + + iso "cunicu.li/go-iso7816" + nk "cunicu.li/go-iso7816/devices/nitrokey" + "cunicu.li/go-iso7816/filter" + "cunicu.li/go-iso7816/test" + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +// rebootCard sends a reboot instruction to the card via the Trussed admin applet. +func rebootCard(t *testing.T) { + test.WithCard(t, filter.IsNitrokey3, func(t *testing.T, card *iso.Card) { + require := require.New(t) + + _, err := card.Select(iso.AidSolokeysAdmin) + require.NoError(err) + + err = nk.Reboot(card) + require.NoError(err) + }) +} + +// withCard is a helper to initialize a card for testing. +func withCard(t *testing.T, vs []vector, reset bool, cb func(t *testing.T, card *secrets.Card)) { + test.WithCard(t, filter.Any, func(t *testing.T, isoCard *iso.Card) { + require := require.New(t) + + oathCard, err := secrets.NewCard(isoCard) + require.NoError(err) + + _, err = oathCard.Select() + require.NoError(err, "Failed to select applet") + + if reset { + err = oathCard.Reset() + require.NoError(err, "Failed to reset applet") + } + + for _, v := range vs { + v := v + err = oathCard.PutOTP(v.ID, v.Algorithm, v.Kind, v.Digits, v.Secret, v.Properties, 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) + } + + // Fix the random source for reproducible tests + oathCard.Rand = rand.New(rand.NewSource(4242)) //nolint:gosec + + cb(t, oathCard) + + err = oathCard.Close() + require.NoError(err) + }) +} diff --git a/code.go b/code.go new file mode 100644 index 0000000..43e4c92 --- /dev/null +++ b/code.go @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "encoding/binary" + "fmt" + "math" +) + +type Code struct { + Digest []byte + Digits int + Type Kind + TouchRequired bool + Truncated bool +} + +// 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.Digest) + code = code % uint32(math.Pow10(c.Digits)) + } else { + hl := len(c.Digest) + o := c.Digest[hl-1] & 0xf + code = binary.BigEndian.Uint32(c.Digest[o:o+4]) & ^uint32(1<<31) + } + + 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..d195e6a --- /dev/null +++ b/code_test.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestOTP(t *testing.T) { + require := require.New(t) + for _, v := range vectors["HOTP"] { + c := secrets.Code{ + Digest: 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..c65e79d --- /dev/null +++ b/credential.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + iso "cunicu.li/go-iso7816" + "cunicu.li/go-iso7816/encoding/tlv" +) + +type Credential struct { + ID string + Login *string + Password *string + Metadata *string + + Kind Kind + Algorithm Algorithm + + Properties *Properties +} + +func (c *Credential) Unmarshal(tvs tlv.TagValues) error { + for _, tv := range tvs { + switch tv.Tag { + case tagCredentialID: + c.ID = string(tv.Value) + + case tagPWSLogin: + s := string(tv.Value) + c.Login = &s + + case tagPWSPassword: + s := string(tv.Value) + c.Password = &s + + case tagPWSMetadata: + s := string(tv.Value) + c.Metadata = &s + + case tagProperties: + if len(tv.Value) != 1 { + return iso.ErrWrongLength + } + + p := Properties(tv.Value[0]) + c.Properties = &p + } + } + + return nil +} + +func (c *Credential) TagValues() tlv.TagValues { + tvs := tlv.TagValues{} + + if c.Login != nil { + tvs = append(tvs, tlv.New(tagPWSLogin, c.Login)) + } + + if c.Password != nil { + tvs = append(tvs, tlv.New(tagPWSPassword, c.Password)) + } + + if c.Metadata != nil { + tvs = append(tvs, tlv.New(tagPWSMetadata, c.Metadata)) + } + + if c.Properties != nil { + tvs = append(tvs, c.Properties.TagValue(false)) + } + + return tvs +} + +func (c *Card) Get(id string) (*Credential, error) { + resp, err := c.send(insGetCredential, 0x00, 0x00, tlv.New(tagCredentialID, id)) + if err != nil { + return nil, err + } + + var creds Credential + + if err := creds.Unmarshal(resp); err != nil { + return nil, err + } + + return &creds, nil +} + +func (c *Card) Rename(oldID, newID string) error { + _, err := c.send(insUpdateCredential, 0x00, 0x00, + tlv.New(tagCredentialID, oldID), + tlv.New(tagCredentialID, newID)) + + return err +} + +func (c *Card) Update(oldID string, n *Credential) error { + tvs := tlv.TagValues{tlv.New(tagCredentialID, oldID)} + tvs = append(tvs, n.TagValues()...) + + _, err := c.send(insUpdateCredential, 0x00, 0x00, tvs...) + return err +} diff --git a/credential_test.go b/credential_test.go new file mode 100644 index 0000000..2227ff9 --- /dev/null +++ b/credential_test.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestPutGetCredential(t *testing.T) { + withCard(t, nil, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + err := card.PutOTP("my-cred", secrets.HMACSHA1, secrets.NotSet, 6, nil, 0, 0) + require.NoError(err) + + cred, err := card.Get("my-cred") + require.NoError(err) + require.Nil(cred.Login) + require.Nil(cred.Password) + require.Nil(cred.Metadata) + + newLogin := "stv0g" + newPassword := "super-s3cr3t-p4ss" + newMetadata := "{ \"created\": \"2024-05-13T12:34:56\" }" + + err = card.Update("my-cred", &secrets.Credential{ + Login: &newLogin, + Password: &newPassword, + Metadata: &newMetadata, + }) + require.NoError(err) + + cred, err = card.Get("my-cred") + require.NoError(err) + require.Equal(newLogin, cred.Login) + require.Equal(newPassword, cred.Password) + require.Equal(newMetadata, cred.Metadata) + }) +} + +func countCredentialsWithID(res []*secrets.ListItem, id string) int { + found := 0 + for _, e := range res { + if e.ID == id { + found++ + } + } + return found +} + +func TestRenameCredential(t *testing.T) { + withCard(t, nil, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + err := card.PutOTP("my-cred", secrets.HMACSHA1, secrets.NotSet, 6, nil, 0, 0) + require.NoError(err) + + res, err := card.List() + require.NoError(err) + + require.Equal(countCredentialsWithID(res, "my-cred"), 1) + + err = card.Rename("my-cred", "my-new-cred") + require.NoError(err) + + res, err = card.List() + require.NoError(err) + + require.Equal(countCredentialsWithID(res, "my-cred"), 0) + require.Equal(countCredentialsWithID(res, "my-new-cred"), 1) + }) +} diff --git a/delete.go b/delete.go new file mode 100644 index 0000000..58eec9a --- /dev/null +++ b/delete.go @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import "cunicu.li/go-iso7816/encoding/tlv" + +// Delete sends a "DELETE" instruction, removing one named OATH credential. +func (c *Card) DeleteCredential(id string) error { + _, err := c.send(insDelete, 0x00, 0x00, tlv.New(tagCredentialID, []byte(id))) + + return err +} diff --git a/delete_test.go b/delete_test.go new file mode 100644 index 0000000..f5acdef --- /dev/null +++ b/delete_test.go @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestDelete(t *testing.T) { + vs := vectorsTOTP[:1] + withCard(t, vs, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + creds, err := card.List() + require.NoError(err) + require.Len(creds, 1) + + err = card.DeleteCredential(vs[0].ID) + 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..6f46025 --- /dev/null +++ b/error.go @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + iso "cunicu.li/go-iso7816" +) + +type Error iso.Code + +// Error return the encapsulated error string. +func (e Error) Error() string { + 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 new file mode 100644 index 0000000..17910f6 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +//go:build !ci + +package secrets_test + +import ( + "fmt" + "log" + "time" + + "cunicu.li/go-iso7816/drivers/pcsc" + "cunicu.li/go-iso7816/filter" + "github.com/ebfe/scard" + + secrets "cunicu.li/go-trussed-secrets" +) + +func Example() { + ctx, err := scard.EstablishContext() + if err != nil { + log.Printf("Failed to establish context: %v", err) + return + } + + sc, err := pcsc.OpenFirstCard(ctx, filter.IsNitrokey3, false) + if err != nil { + log.Printf("Failed to connect to card: %v", err) + return + } + + c, err := secrets.NewCard(sc) + if err != nil { + log.Print(err) + return + } + + defer c.Close() + + // Fix the clock + c.Clock = func() time.Time { + return time.Unix(59, 0) + } + + // Select the Trussed Secrets applet + 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 = c.PutOTP("testvector", secrets.HMACSHA1, secrets.TOTP, 8, []byte("12345678901234567890"), 0, 0); err != nil { + log.Printf("Failed to put: %v", err) + return + } + + names, err := c.List() + if err != nil { + log.Printf("Failed to list: %v", err) + return + } + + for _, name := range names { + fmt.Printf("Name: %s\n", name) + } + + otp, _ := c.Calculate("testvector") + fmt.Printf("OTP: %s\n", otp.OTP()) + + // Output: + // Name: testvector (HMAC-SHA1, TOTP) + // OTP: 94287082 +} diff --git a/flake.nix b/flake.nix index 68cab8d..5050e6e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 Steffen Vogel +# SPDX-FileCopyrightText: 2024 Steffen Vogel # SPDX-License-Identifier: Apache-2.0 { inputs = { @@ -13,7 +13,10 @@ flake-utils.lib.eachDefaultSystem ( system: let - pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ @@ -24,7 +27,7 @@ reuse pcsclite pcsc-tools - pynitrokey + # pynitrokey ]; }; diff --git a/go.mod b/go.mod index 38df69f..29ff3aa 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,22 @@ -// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-FileCopyrightText: 2024 Steffen Vogel // SPDX-License-Identifier: Apache-2.0 -module cunicu.li/skeleton +module cunicu.li/go-trussed-secrets go 1.22.2 + +require ( + cunicu.li/go-iso7816 v0.5.2 + github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.23.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace cunicu.li/go-iso7816 => ../go-iso7816 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e13e0ed --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +cunicu.li/go-iso7816 v0.5.2 h1:hdGm6xLZ5//FjRSFxMRfuqfEFDIu6bIlKreKrXH6Fx8= +cunicu.li/go-iso7816 v0.5.2/go.mod h1:OwF8/IxfJAi/GEuMv3cv7jXFmbto+7lDwl2eknMucoA= +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= +github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 h1:HYAhfGa9dEemCZgGZWL5AvVsctBCsHxl2CI0HUXzHQE= +github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7/go.mod h1:BkYEeWL6FbT4Ek+TcOBnPzEKnL7kOq2g19tTQXkorHY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kind.go b/kind.go new file mode 100644 index 0000000..db632b3 --- /dev/null +++ b/kind.go @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import "fmt" + +const ( + HOTP Kind = 0x10 // HOTP calculates OTP as HOTP, against the internal counter. + TOTP Kind = 0x20 // TOTP calculates OTP as TOTP, against the provided challenge. + HOTPReverse Kind = 0x30 // HOTPReverse calculates HOTP code, and compare against the provided one. + HMAC Kind = 0x40 // HMAC calculates HMAC-challenge value. + NotSet Kind = 0xF0 // NotSet is used for password safe entries. +) + +// Kind denotes the kind of derivation used for the one-time password. +type Kind byte + +// String returns a string representation of the type. +func (t Kind) String() string { + switch t { + case HOTP: + return "HOTP" + + case TOTP: + return "TOTP" + + case HOTPReverse: + return "Reverse HOTP" + + case HMAC: + return "HMAC" + + default: + return fmt.Sprintf("unknown %x", byte(t)) + } +} diff --git a/list.go b/list.go new file mode 100644 index 0000000..28adfcd --- /dev/null +++ b/list.go @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "fmt" + "strings" +) + +// ListItem encapsulates the result of the "LIST" instruction. +type ListItem struct { + ID string + Kind Kind + Algorithm Algorithm + Properties Properties +} + +// String returns a string representation of the algorithm. +func (n *ListItem) String() string { + details := []string{ + n.Algorithm.String(), + n.Kind.String(), + } + + if n.Properties > 0 { + details = append(details, n.Properties.String()) + } + + return fmt.Sprintf("%s (%s)", n.ID, strings.Join(details, ", ")) +} + +// List sends a "LIST" instruction, return a list of credentials. +func (c *Card) List() ([]*ListItem, error) { + var items []*ListItem + var version byte = 1 + + tvs, err := c.sendRaw(insList, 0x00, 0x00, []byte{version}) + if err != nil { + return nil, err + } + + for _, tv := range tvs { + switch tv.Tag { + case tagNameList: + item := &ListItem{} + + item.Algorithm = Algorithm(tv.Value[0] & 0x0f) + item.Kind = Kind(tv.Value[0] & 0xf0) + + if c.info.SupportsExtendedList() { + l := len(tv.Value) + + item.Properties = Properties(tv.Value[l-1]) + item.ID = string(tv.Value[1 : l-1]) + } else { + item.Properties = 0 + item.ID = string(tv.Value[1:]) + } + + items = append(items, item) + + default: + return nil, fmt.Errorf("%w (%#x)", errUnknownTag, tv.Tag) + } + } + + return items, nil +} diff --git a/list_test.go b/list_test.go new file mode 100644 index 0000000..6c14084 --- /dev/null +++ b/list_test.go @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestList(t *testing.T) { + withCard(t, vectorsMix, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + res, err := card.List() + require.NoError(err) + require.Len(res, len(vectorsMix)) + + vs := map[string]*vector{} + for _, v := range vectorsMix { + v := v + vs[v.ID] = &v + } + + fmt.Println(vs) + + for _, r := range res { + v, ok := vs[r.ID] + require.True(ok) + + require.Equal(v.ID, r.ID) + require.Equal(v.Algorithm, r.Algorithm) + require.Equal(v.Kind, r.Kind) + require.Equal(v.Properties, r.Properties) + } + }) +} diff --git a/main.go b/main.go deleted file mode 100644 index 66fa1ba..0000000 --- a/main.go +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Steffen Vogel -// SPDX-License-Identifier: Apache-2.0 - -package skeleton - -func Hello() string { - return "hello" -} diff --git a/main_test.go b/main_test.go deleted file mode 100644 index ca13502..0000000 --- a/main_test.go +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Steffen Vogel -// SPDX-License-Identifier: Apache-2.0 - -package skeleton_test - -import ( - "testing" - - "cunicu.li/skeleton" -) - -func TestHello(t *testing.T) { - if skeleton.Hello() != "hello" { - t.Error() - } -} diff --git a/mockdata/TestCalculate/nk3-1.7.0 b/mockdata/TestCalculate/nk3-1.7.0 new file mode 100644 index 0000000..fd94650 --- /dev/null +++ b/mockdata/TestCalculate/nk3-1.7.0 @@ -0,0 +1,72 @@ +mockfile + +file.version v2 +file.created 2024-05-15T13:07:29+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state negotiable,present,powered + +# start end method +on 0.000 0.000 BeginTransaction +on 0.024 0.024 Transmit 00a4040007a0000005272101ff 7903040d00710803ac92ab2d42a8c18f04faabf2849000 +on 55.132 55.132 Transmit 0004dead00 9000 +on 8008.891 8008.891 Transmit 000100002b710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930780000 9000 +on 8854.155 8854.155 Transmit 0001000037710f726663363233382d746573742d3032732222083132333435363738393031323334353637383930313233343536373839303132780000 9000 +on 9340.305 9340.305 Transmit 000100002b710f726663363233382d746573742d3034731621083132333435363738393031323334353637383930780000 9000 +on 10014.848 10014.848 Transmit 0001000037710f726663363233382d746573742d3035732222083132333435363738393031323334353637383930313233343536373839303132780000 9000 +on 10911.826 10911.826 Transmit 000100002b710f726663363233382d746573742d3037731621083132333435363738393031323334353637383930780000 9000 +on 12066.067 12066.067 Transmit 0001000037710f726663363233382d746573742d3038732222083132333435363738393031323334353637383930313233343536373839303132780000 9000 +on 13504.447 13504.447 Transmit 000100002b710f726663363233382d746573742d3130731621083132333435363738393031323334353637383930780000 9000 +on 15280.664 15280.664 Transmit 0001000037710f726663363233382d746573742d3131732222083132333435363738393031323334353637383930313233343536373839303132780000 9000 +on 17811.346 17811.346 Transmit 000100002b710f726663363233382d746573742d3133731621083132333435363738393031323334353637383930780000 9000 +on 19586.852 19586.852 Transmit 0001000037710f726663363233382d746573742d3134732222083132333435363738393031323334353637383930313233343536373839303132780000 9000 +on 21741.762 21741.762 Transmit 000100002b710f726663363233382d746573742d3136731621083132333435363738393031323334353637383930780000 9000 +on 24306.787 24306.787 Transmit 0001000037710f726663363233382d746573742d3137732222083132333435363738393031323334353637383930313233343536373839303132780000 9000 +on 27364.078 27364.078 Transmit 000100002b710f726663343232362d746573742d3030731611063132333435363738393031323334353637383930780000 9000 +on 30938.273 30938.273 Transmit 0001000031710f726663343232362d746573742d303173161106313233343536373839303132333435363738393078007a040000000100 9000 +on 35619.149 35619.149 Transmit 0001000031710f726663343232362d746573742d303273161106313233343536373839303132333435363738393078007a040000000200 9000 +on 39162.459 39162.459 Transmit 0001000031710f726663343232362d746573742d303373161106313233343536373839303132333435363738393078007a040000000300 9000 +on 43278.632 43278.632 Transmit 0001000031710f726663343232362d746573742d303473161106313233343536373839303132333435363738393078007a040000000400 9000 +on 48076.084 48076.084 Transmit 0001000031710f726663343232362d746573742d303573161106313233343536373839303132333435363738393078007a040000000500 9000 +on 53631.658 53631.658 Transmit 0001000031710f726663343232362d746573742d303673161106313233343536373839303132333435363738393078007a040000000600 9000 +on 61755.756 61755.756 Transmit 0001000031710f726663343232362d746573742d303773161106313233343536373839303132333435363738393078007a040000000700 9000 +on 66432.322 66432.322 Transmit 0001000031710f726663343232362d746573742d303873161106313233343536373839303132333435363738393078007a040000000800 9000 +on 71543.442 71543.442 Transmit 0001000031710f726663343232362d746573742d303973161106313233343536373839303132333435363738393078007a040000000900 9000 +on 77348.700 77348.700 Transmit 00010000397117726663343232362d386469676974732d746573742d303973161108313233343536373839303132333435363738393078007a040000000900 9000 +on 83902.829 83902.829 Transmit 00a200011b710f726663363233382d746573742d30317408000000000000000100 76050841397eea9000 +on 84082.180 84082.180 Transmit 00a200011b710f726663363233382d746573742d30327408000000000000000100 7605082c78e04e9000 +on 84263.220 84263.220 Transmit 00a200011b710f726663363233382d746573742d3034740800000000023523ec00 7605083610f84c9000 +on 84470.791 84470.791 Transmit 00a200011b710f726663363233382d746573742d3035740800000000023523ec00 7605085d7713269000 +on 84679.188 84679.188 Transmit 00a200011b710f726663363233382d746573742d3037740800000000023523ed00 76050818ade8a79000 +on 84858.071 84858.071 Transmit 00a200011b710f726663363233382d746573742d3038740800000000023523ed00 760508458ff6929000 +on 85066.079 85066.079 Transmit 00a200011b710f726663363233382d746573742d31307408000000000273ef0700 760508291165649000 +on 85244.726 85244.726 Transmit 00a200011b710f726663363233382d746573742d31317408000000000273ef0700 76050805790da09000 +on 85451.780 85451.780 Transmit 00a200011b710f726663363233382d746573742d313374080000000003f940aa00 7605087b56b13d9000 +on 85656.761 85656.761 Transmit 00a200011b710f726663363233382d746573742d313474080000000003f940aa00 7605086abbe5499000 +on 85836.078 85836.078 Transmit 00a200011b710f726663363233382d746573742d313674080000000027bc86aa00 760508575783aa9000 +on 86040.761 86040.761 Transmit 00a200011b710f726663363233382d746573742d313774080000000027bc86aa00 7605082e5b55ea9000 +on 86245.732 86245.732 Transmit 00a200011b710f726663343232362d746573742d30307408ffffffff848c338000 7605064c93cf189000 +on 86660.134 86660.134 Transmit 00a200011b710f726663343232362d746573742d30317408ffffffff848c338000 76050641397eea9000 +on 87081.714 87081.714 Transmit 00a200011b710f726663343232362d746573742d30327408ffffffff848c338000 760506082fef309000 +on 87509.941 87509.941 Transmit 00a200011b710f726663343232362d746573742d30337408ffffffff848c338000 76050666ef76559000 +on 88002.105 88002.105 Transmit 00a200011b710f726663343232362d746573742d30347408ffffffff848c338000 76050661c5938a9000 +on 88500.046 88500.046 Transmit 00a200011b710f726663343232362d746573742d30357408ffffffff848c338000 76050633c083d49000 +on 89005.050 89005.050 Transmit 00a200011b710f726663343232362d746573742d30367408ffffffff848c338000 7605067256c0329000 +on 89439.676 89439.676 Transmit 00a200011b710f726663343232362d746573742d30377408ffffffff848c338000 76050604e5b3979000 +on 89957.669 89957.669 Transmit 00a200011b710f726663343232362d746573742d30387408ffffffff848c338000 7605062823443f9000 +on 90398.024 90398.024 Transmit 00a200011b710f726663343232362d746573742d30397408ffffffff848c338000 7605062679dc699000 +on 91400.319 91400.319 Transmit 00a20001237117726663343232362d386469676974732d746573742d30397408ffffffff848c338000 7605082679dc699000 +on 91795.565 91795.565 EndTransaction diff --git a/mockdata/TestCalculateHOTPCounterIncrement/nk3-1.7.0 b/mockdata/TestCalculateHOTPCounterIncrement/nk3-1.7.0 new file mode 100644 index 0000000..b51f65e --- /dev/null +++ b/mockdata/TestCalculateHOTPCounterIncrement/nk3-1.7.0 @@ -0,0 +1,37 @@ +mockfile + +file.version v2 +file.created 2024-05-15T12:32:11+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state present,powered,negotiable + +# start end method +on 0.000 0.000 BeginTransaction +on 0.025 0.025 Transmit 00a4040007a0000005272101ff 7903040d0071082c39689eed1942a68f04faabf2849000 +on 70.989 70.989 Transmit 0004dead00 9000 +on 5465.532 5465.532 Transmit 000100002b710f726663343232362d746573742d3030731611063132333435363738393031323334353637383930780000 9000 +on 6357.222 6357.222 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 7605064c93cf189000 +on 6779.785 6779.785 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 76050641397eea9000 +on 7209.746 7209.746 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 760506082fef309000 +on 7645.424 7645.424 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 76050666ef76559000 +on 8088.452 8088.452 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 76050661c5938a9000 +on 8538.132 8538.132 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 76050633c083d49000 +on 8994.428 8994.428 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 7605067256c0329000 +on 9457.776 9457.776 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 76050604e5b3979000 +on 9927.822 9927.822 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 7605062823443f9000 +on 10404.372 10404.372 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 7605062679dc699000 +on 10888.183 10888.183 EndTransaction diff --git a/mockdata/TestCalculateHOTPCounterInit/nk3-1.7.0 b/mockdata/TestCalculateHOTPCounterInit/nk3-1.7.0 new file mode 100644 index 0000000..60758bd --- /dev/null +++ b/mockdata/TestCalculateHOTPCounterInit/nk3-1.7.0 @@ -0,0 +1,48 @@ +mockfile + +file.version v2 +file.created 2024-05-15T12:31:40+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state present,powered,negotiable + +# start end method +on 0.000 0.000 BeginTransaction +on 0.026 0.026 Transmit 00a4040007a0000005272101ff 7903040d007108dc642dc98309352a8f04faabf2849000 +on 61.225 61.225 Transmit 0004dead00 9000 +on 3396.568 3396.568 Transmit 000100002b710f726663343232362d746573742d3030731611063132333435363738393031323334353637383930780000 9000 +on 4245.163 4245.163 Transmit 0001000031710f726663343232362d746573742d303173161106313233343536373839303132333435363738393078007a040000000100 9000 +on 4772.488 4772.488 Transmit 0001000031710f726663343232362d746573742d303273161106313233343536373839303132333435363738393078007a040000000200 9000 +on 5501.740 5501.740 Transmit 0001000031710f726663343232362d746573742d303373161106313233343536373839303132333435363738393078007a040000000300 9000 +on 6460.785 6460.785 Transmit 0001000031710f726663343232362d746573742d303473161106313233343536373839303132333435363738393078007a040000000400 9000 +on 7692.250 7692.250 Transmit 0001000031710f726663343232362d746573742d303573161106313233343536373839303132333435363738393078007a040000000500 9000 +on 9233.886 9233.886 Transmit 0001000031710f726663343232362d746573742d303673161106313233343536373839303132333435363738393078007a040000000600 9000 +on 11124.833 11124.833 Transmit 0001000031710f726663343232362d746573742d303773161106313233343536373839303132333435363738393078007a040000000700 9000 +on 13797.868 13797.868 Transmit 0001000031710f726663343232362d746573742d303873161106313233343536373839303132333435363738393078007a040000000800 9000 +on 15730.116 15730.116 Transmit 0001000031710f726663343232362d746573742d303973161106313233343536373839303132333435363738393078007a040000000900 9000 +on 18052.020 18052.020 Transmit 00010000397117726663343232362d386469676974732d746573742d303973161108313233343536373839303132333435363738393078007a040000000900 9000 +on 20809.382 20809.382 Transmit 00a200011b710f726663343232362d746573742d30307408000000000000000100 7605064c93cf189000 +on 21260.901 21260.901 Transmit 00a200011b710f726663343232362d746573742d30317408000000000000000100 76050641397eea9000 +on 21718.899 21718.899 Transmit 00a200011b710f726663343232362d746573742d30327408000000000000000100 760506082fef309000 +on 22183.524 22183.524 Transmit 00a200011b710f726663343232362d746573742d30337408000000000000000100 76050666ef76559000 +on 22654.574 22654.574 Transmit 00a200011b710f726663343232362d746573742d30347408000000000000000100 76050661c5938a9000 +on 23132.247 23132.247 Transmit 00a200011b710f726663343232362d746573742d30357408000000000000000100 76050633c083d49000 +on 24130.529 24130.529 Transmit 00a200011b710f726663343232362d746573742d30367408000000000000000100 7605067256c0329000 +on 24548.474 24548.474 Transmit 00a200011b710f726663343232362d746573742d30377408000000000000000100 76050604e5b3979000 +on 24973.941 24973.941 Transmit 00a200011b710f726663343232362d746573742d30387408000000000000000100 7605062823443f9000 +on 25405.756 25405.756 Transmit 00a200011b710f726663343232362d746573742d30397408000000000000000100 7605062679dc699000 +on 25844.780 25844.780 Transmit 00a20001237117726663343232362d386469676974732d746573742d30397408000000000000000100 7605082679dc699000 +on 26290.468 26290.468 EndTransaction diff --git a/mockdata/TestDelete/nk3-1.7.0 b/mockdata/TestDelete/nk3-1.7.0 new file mode 100644 index 0000000..9efe34b --- /dev/null +++ b/mockdata/TestDelete/nk3-1.7.0 @@ -0,0 +1,30 @@ +mockfile + +file.version v2 +file.created 2024-05-15T10:58:55+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state powered,negotiable,present + +# start end method +on 0.000 0.000 BeginTransaction +on 0.027 0.027 Transmit 00a4040007a0000005272101ff 7903040d007108bb91749eb6fc2bd48f04faabf2849000 +on 61.389 61.389 Transmit 0004dead00 9000 +on 2128.343 2128.343 Transmit 000100002b710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930780000 9000 +on 2978.991 2978.991 Transmit 00a1000000 721021726663363233382d746573742d30319000 +on 3206.980 3206.980 Transmit 0002000011710f726663363233382d746573742d303100 9000 +on 10156.377 10156.377 Transmit 00a1000000 9000 +on 10235.364 10235.364 EndTransaction diff --git a/mockdata/TestList/nk3-1.7.0 b/mockdata/TestList/nk3-1.7.0 new file mode 100644 index 0000000..45d0ff5 --- /dev/null +++ b/mockdata/TestList/nk3-1.7.0 @@ -0,0 +1,31 @@ +mockfile + +file.version v2 +file.created 2024-05-15T11:38:30+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state powered,negotiable,present + +# start end method +on 0.000 0.000 BeginTransaction +on 0.027 0.027 Transmit 00a4040007a0000005272101ff 7903040d0071087fd0031ed2e180c48f04faabf2849000 +on 82.139 82.139 Transmit 0004dead00 9000 +on 2629.429 2629.429 Transmit 0001000027710b6d69782d746573742d3031731621083132333435363738393031323334353637383930780200 9000 +on 5099.563 5099.563 Transmit 0001000033710b6d69782d746573742d3032732212083132333435363738393031323334353637383930313233343536373839303132780000 9000 +on 5440.262 5440.262 Transmit 0001000027710b6d69782d746573742d3033731631083132333435363738393031323334353637383930780000 9000 +on 5924.692 5924.692 Transmit 0001000027710b6d69782d746573742d3034731641063132333435363738393031323334353637383930780000 9000 +on 6589.975 6589.975 Transmit 00a10000010100 720d126d69782d746573742d303200720d216d69782d746573742d303101720d416d69782d746573742d303400720d316d69782d746573742d3033009000 +on 7255.109 7255.109 EndTransaction diff --git a/mockdata/TestPIN/nk3-1.7.0 b/mockdata/TestPIN/nk3-1.7.0 new file mode 100644 index 0000000..c0faaba --- /dev/null +++ b/mockdata/TestPIN/nk3-1.7.0 @@ -0,0 +1,36 @@ +mockfile + +file.version v2 +file.created 2024-05-15T10:55:48+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 33 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state powered,negotiable,present + +# start end method +on 0.000 0.000 BeginTransaction +on 0.027 0.027 Transmit 00a4040007a0000005272101ff 7903040d00710881118919148dc4748f04faabf2849000 +on 64.105 64.105 Transmit 0004dead00 9000 +on 5100.896 5100.896 Transmit 00a4040007a0000005272101ff 7903040d0071083c4cb2676ff53e378f04faabf2849000 +on 5175.491 5175.491 Transmit 00b200000680043132333400 6982 +on 5179.779 5179.779 Transmit 0001000027710b756e656e63727970746564731621083132333435363738393031323334353637383930780000 9000 +on 6042.165 6042.165 Transmit 00010000257109656e63727970746564731621083132333435363738393031323334353637383930780400 6982 +on 6430.143 6430.143 Transmit 00a1000000 720c21756e656e637279707465649000 +on 6666.747 6666.747 Transmit 00b400000680043133333800 9000 +on 7767.714 7767.714 Transmit 00b200000680043133333800 9000 +on 7786.679 7786.679 Transmit 00010000257109656e63727970746564731621083132333435363738393031323334353637383930780400 9000 +on 8398.687 8398.687 Transmit 00b200000680043133333800 9000 +on 9828.685 9828.685 Transmit 00a1000000 720a21656e63727970746564720c21756e656e637279707465649000 +on 10331.389 10331.389 EndTransaction diff --git a/mockdata/TestPIN/nk3-1.7.0#01 b/mockdata/TestPIN/nk3-1.7.0#01 new file mode 100644 index 0000000..9df538e --- /dev/null +++ b/mockdata/TestPIN/nk3-1.7.0#01 @@ -0,0 +1,24 @@ +mockfile + +file.version v2 +file.created 2024-05-15T10:55:50+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state present,powered,negotiable + +# start end method +on 0.000 0.000 Transmit 00a4040009a00000084700000001ff 9000 +on 1.697 1.697 Transmit 00530000 diff --git a/mockdata/TestPIN/nk3-1.7.0#02 b/mockdata/TestPIN/nk3-1.7.0#02 new file mode 100644 index 0000000..39aee68 --- /dev/null +++ b/mockdata/TestPIN/nk3-1.7.0#02 @@ -0,0 +1,36 @@ +mockfile + +file.version v2 +file.created 2024-05-15T10:55:56+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state present,powered,negotiable + +# start end method +on 0.000 0.000 BeginTransaction +on 0.025 0.025 Transmit 00a4040007a0000005272101ff 7903040d008201088f04faabf2849000 +on 105.051 105.051 Transmit 00a4040007a0000005272101ff 7903040d008201088f04faabf2849000 +on 183.751 183.751 Transmit 00a20001157109656e637279707465647408000000000000000100 6a82 +on 273.555 273.555 Transmit 0001000032710a656e6372797074656432732222083132333435363738393031323334353637383930313233343536373839303132780400 6982 +on 865.406 865.406 Transmit 00b400000680043133333900 6982 +on 870.044 870.044 Transmit 00b200000680043133333700 6300 +on 1719.157 1719.157 Transmit 00b200000680043133333800 9000 +on 1751.117 1751.117 Transmit 0001000032710a656e6372797074656432732222083132333435363738393031323334353637383930313233343536373839303132780400 9000 +on 2568.376 2568.376 Transmit 00b200000680043133333800 9000 +on 3493.126 3493.126 Transmit 00a1000000 720b22656e6372797074656432720a21656e63727970746564720c21756e656e637279707465649000 +on 4224.659 4224.659 Transmit 00b200000680043133333800 9000 +on 5560.298 5560.298 Transmit 00a2000116710a656e63727970746564327408000000000000000100 7605082c78e04e9000 +on 5799.569 5799.569 EndTransaction diff --git a/mockdata/TestPutGetCredential/nk3-1.7.0 b/mockdata/TestPutGetCredential/nk3-1.7.0 new file mode 100644 index 0000000..e4cba0b --- /dev/null +++ b/mockdata/TestPutGetCredential/nk3-1.7.0 @@ -0,0 +1,30 @@ +mockfile + +file.version v2 +file.created 2024-05-15T10:59:05+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state powered,negotiable,present + +# start end method +on 0.000 0.000 BeginTransaction +on 0.031 0.031 Transmit 00a4040007a0000005272101ff 7903040d0071087b799430b406dbdd8f04faabf2849000 +on 70.490 70.490 Transmit 0004dead00 9000 +on 1996.315 1996.315 Transmit 000100002371076d792d637265647316f1060000000000000000000000000000000000000000780000 9000 +on 2887.900 2887.900 Transmit 00b500000971076d792d6372656400 7801f171076d792d637265649000 +on 2977.680 2977.680 Transmit 00b700002d71076d792d6372656483086d792d6c6f67696e840b6d792d70617373776f7264850b6d792d6d6574616461746100 9000 +on 5822.195 5822.195 Transmit 00b500000971076d792d6372656400 7801f171076d792d6372656483086d792d6c6f67696e840b6d792d70617373776f7264850b6d792d6d657461646174619000 +on 5915.327 5915.327 EndTransaction diff --git a/mockdata/TestRenameCredential/nk3-1.7.0 b/mockdata/TestRenameCredential/nk3-1.7.0 new file mode 100644 index 0000000..ea95c35 --- /dev/null +++ b/mockdata/TestRenameCredential/nk3-1.7.0 @@ -0,0 +1,30 @@ +mockfile + +file.version v2 +file.created 2024-05-15T11:01:41+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state present,powered,negotiable + +# start end method +on 0.000 0.000 BeginTransaction +on 0.029 0.029 Transmit 00a4040007a0000005272101ff 7903040d007108d71c955730d7e3758f04faabf2849000 +on 40.177 40.177 Transmit 0004dead00 9000 +on 1489.564 1489.564 Transmit 000100002371076d792d637265647316f1060000000000000000000000000000000000000000780000 9000 +on 2233.358 2233.358 Transmit 00a1000000 7208f16d792d637265649000 +on 2398.539 2398.539 Transmit 00b700001671076d792d63726564710b6d792d6e65772d6372656400 9000 +on 4782.172 4782.172 Transmit 00a1000000 720cf16d792d6e65772d637265649000 +on 4976.854 4976.854 EndTransaction diff --git a/mockdata/TestReset/nk3-1.7.0 b/mockdata/TestReset/nk3-1.7.0 new file mode 100644 index 0000000..b89c2c9 --- /dev/null +++ b/mockdata/TestReset/nk3-1.7.0 @@ -0,0 +1,29 @@ +mockfile + +file.version v2 +file.created 2024-05-15T10:55:26+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 33 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state present,powered,negotiable + +# start end method +on 0.000 0.000 BeginTransaction +on 0.026 0.026 Transmit 00a4040007a0000005272101ff 7903040d0071085019ecb23ba181758f04faabf2849000 +on 55.063 55.063 Transmit 0004dead00 9000 +on 2266.318 2266.318 Transmit 000100002b710f726663363233382d746573742d3031731621083132333435363738393031323334353637383930780000 9000 +on 3086.283 3086.283 Transmit 0004dead00 9000 +on 6816.134 6816.134 Transmit 00a1000000 9000 +on 6887.184 6887.184 EndTransaction diff --git a/mockdata/TestVerifyCode/nk3-1.7.0 b/mockdata/TestVerifyCode/nk3-1.7.0 new file mode 100644 index 0000000..0a0733d --- /dev/null +++ b/mockdata/TestVerifyCode/nk3-1.7.0 @@ -0,0 +1,37 @@ +mockfile + +file.version v2 +file.created 2024-05-15T13:15:27+02:00 +file.creator stv0g@rise + +meta attr.channel.type usb +meta attr.channel.usb.addr 34 +meta attr.channel.usb.bus 5 +meta attr.ifd.serial +meta attr.ifd.version 1.7.0 +meta attr.name.friendly Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.system Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta attr.name.vendor Nitrokey +meta nitrokey.uuid faabf284f9848452874c8f810eb04be0 +meta nitrokey.version 1.7.0 +meta status.active_protocol t1,any +meta status.atr 3b8f01805d4e6974726f6b657900000000006a +meta status.reader Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00 +meta status.state negotiable,present,powered + +# start end method +on 0.000 0.000 BeginTransaction +on 0.029 0.029 Transmit 00a4040007a0000005272101ff 7903040d0071089d4abcb278a424508f04faabf2849000 +on 82.409 82.409 Transmit 0004dead00 9000 +on 3444.643 3444.643 Transmit 0001000028710c726576657273652d686f7470731631063132333435363738393031323334353637383930780000 9000 +on 4216.003 4216.003 Transmit 00b1000014710c726576657273652d686f74707504000b861800 77009000 +on 4459.979 4459.979 Transmit 00b1000014710c726576657273652d686f747075040004616a00 77009000 +on 4710.995 4710.995 Transmit 00b1000014710c726576657273652d686f7470750400057af000 77009000 +on 4969.266 4969.266 Transmit 00b1000014710c726576657273652d686f74707504000ecad500 77009000 +on 5234.260 5234.260 Transmit 00b1000014710c726576657273652d686f747075040005298a00 77009000 +on 5505.804 5505.804 Transmit 00b1000014710c726576657273652d686f747075040003e2d400 77009000 +on 5783.525 5783.525 Transmit 00b1000014710c726576657273652d686f74707504000464b200 77009000 +on 6068.870 6068.870 Transmit 00b1000014710c726576657273652d686f7470750400027b1700 77009000 +on 6360.518 6360.518 Transmit 00b1000014710c726576657273652d686f74707504000619ff00 77009000 +on 6658.775 6658.775 Transmit 00b1000014710c726576657273652d686f747075040007f12900 77009000 +on 6963.755 6963.755 EndTransaction diff --git a/pin.go b/pin.go new file mode 100644 index 0000000..8d5d6c4 --- /dev/null +++ b/pin.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "errors" + + "cunicu.li/go-iso7816/encoding/tlv" +) + +var ErrNoPINSet = errors.New("no PIN has been configured") + +// SetPIN sets an initial PIN. +// This command requires no prior authentication. +// No previous PIN must be configured. +func (c *Card) SetPIN(password []byte) error { + _, err := c.send(insSetPIN, 0x00, 0x00, + tlv.New(tagPassword, password), + ) + return err +} + +// ChangePIN changes the PIN. +// This command no prior authentication. +// The old password must be provided. +func (c *Card) ChangePIN(oldPassword, newPassword []byte) error { + _, err := c.send(insChangePIN, 0x00, 0x00, + tlv.New(tagPassword, oldPassword), + tlv.New(tagNewPassword, newPassword), + ) + return err +} + +// VerifyPIN checks the provided PIN code. +// This command requires no prior authentication. +func (c *Card) VerifyPIN(password []byte) error { + _, err := c.send(insVerifyPIN, 0x00, 0x00, + tlv.New(tagPassword, password), + ) + return err +} diff --git a/pin_test.go b/pin_test.go new file mode 100644 index 0000000..1738f94 --- /dev/null +++ b/pin_test.go @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "testing" + + iso "cunicu.li/go-iso7816" + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestPIN(t *testing.T) { + withCard(t, nil, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + sel, err := card.Select() + require.NoError(err) + require.False(sel.HasActivePIN()) + require.Nil(sel.Challenge) + require.Nil(sel.Algorithm) + + // VerifyPIN should fail if not PIN is set + err = card.VerifyPIN([]byte("1234")) + require.ErrorIs(err, secrets.Error(iso.ErrSecurityStatusNotSatisfied)) + + // Adding an unencrypted credential must always succeed + err = card.PutOTP("unencrypted", secrets.HMACSHA1, secrets.TOTP, 8, testSecretSHA1, 0, 0) + require.NoError(err) + + // Adding a PIN encrypted credential must fail if no PIN has been set + err = card.PutOTP("encrypted", secrets.HMACSHA1, secrets.TOTP, 8, testSecretSHA1, secrets.PINEncrypted, 0) + require.ErrorIs(err, secrets.Error(iso.ErrSecurityStatusNotSatisfied)) + + it, err := card.List() + require.NoError(err) + require.Len(it, 1) + + // Set PIN + err = card.SetPIN([]byte("1338")) + require.NoError(err) + + // Verify PIN + err = card.VerifyPIN([]byte("1338")) + require.NoError(err) + + // Adding a PIN encrypted credential must succeed after PIN has been set + err = card.PutOTP("encrypted", secrets.HMACSHA1, secrets.TOTP, 8, testSecretSHA1, secrets.PINEncrypted, 0) + require.NoError(err) + + // Verify valid PIN + err = card.VerifyPIN([]byte("1338")) + require.NoError(err) + + it, err = card.List() + require.NoError(err) + require.Len(it, 2) + }) + + rebootCard(t) + + withCard(t, nil, false, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + sel, err := card.Select() + require.NoError(err) + require.True(sel.HasActivePIN()) + require.Equal(uint32(8), sel.PINCounter) + require.Nil(sel.Challenge) + require.Nil(sel.Algorithm) + + _, err = card.Calculate("encrypted") + require.ErrorIs(err, secrets.Error(iso.ErrFileOrAppNotFound)) + + err = card.PutOTP("encrypted2", secrets.HMACSHA256, secrets.TOTP, 8, testSecretSHA256, secrets.PINEncrypted, 0) + require.ErrorIs(err, secrets.Error(iso.ErrSecurityStatusNotSatisfied)) + + // Set PIN again should fail + err = card.SetPIN([]byte("1339")) + require.ErrorIs(err, secrets.Error(iso.ErrSecurityStatusNotSatisfied)) + + // Verify invalid PIN + err = card.VerifyPIN([]byte("1337")) + require.ErrorIs(err, secrets.Error(iso.ErrUnspecifiedWarningModified)) + + // Verify valid PIN + err = card.VerifyPIN([]byte("1338")) + require.NoError(err) + + err = card.PutOTP("encrypted2", secrets.HMACSHA256, secrets.TOTP, 8, testSecretSHA256, secrets.PINEncrypted, 0) + require.NoError(err) + + // Verify valid PIN + err = card.VerifyPIN([]byte("1338")) + require.NoError(err) + + it, err := card.List() + require.NoError(err) + require.Len(it, 3) + + err = card.VerifyPIN([]byte("1338")) + require.NoError(err) + + res, err := card.Calculate("encrypted2") + require.NoError(err) + require.Equal("46119246", res) + }) +} diff --git a/properties.go b/properties.go new file mode 100644 index 0000000..eea9974 --- /dev/null +++ b/properties.go @@ -0,0 +1,51 @@ +package secrets + +import ( + "strings" + + "cunicu.li/go-iso7816/encoding/tlv" +) + +type Properties byte + +const ( + TouchRequired Properties = 0x01 + PINEncrypted Properties = 0x02 + PWSDataExists +) + +func (p Properties) String() string { + strs := []string{} + + if p&PINEncrypted != 0 { + strs = append(strs, "pin-encrypted") + } + + if p&PWSDataExists != 0 { + strs = append(strs, "password-safe") + } + + if p&TouchRequired != 0 { + strs = append(strs, "touch-required") + } + + return strings.Join(strs, ", ") +} + +func (p Properties) TagValue(skipLength bool) tlv.TagValue { + var q byte + + if p&TouchRequired != 0 { + q |= 0x02 + } + + if p&PINEncrypted != 0 { + q |= 0x04 + } + + return tlv.TagValue{ + Tag: tagProperties, + Value: []byte{q}, + SkipLength: skipLength, + } +} diff --git a/put.go b/put.go new file mode 100644 index 0000000..0ab5113 --- /dev/null +++ b/put.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "encoding/binary" + "errors" + "fmt" + + "cunicu.li/go-iso7816/encoding/tlv" +) + +var ( + ErrNameTooLong = errors.New("name too long)") + ErrInvalidNumberOfDigits = errors.New("number of digits must be 6 or 8") +) + +// PutPasswords adds a mew password safe. +func (c *Card) PutPassword(id string, props Properties) error { + return c.PutOTP(id, HMACSHA1, NotSet, 6, padKey(nil), props, 0) +} + +// PutOTP adds a new OTP credential. +func (c *Card) PutOTP(id string, alg Algorithm, kind Kind, digits int, secret []byte, props Properties, counter uint32) error { + if l := len(id); l > 64 { + return fmt.Errorf("%w: (%d > 64)", ErrNameTooLong, l) + } + + if kind != HMAC && (digits != 6 && digits != 8) { + return ErrInvalidNumberOfDigits + } + + if alg == HMACSHA512 { + return fmt.Errorf("%w: This hash algorithm is not supported by the firmware", errors.ErrUnsupported) + } + + secret = shortenKey(secret, alg) + secret = padKey(secret) + + tvs := []tlv.TagValue{ + tlv.New(tagCredentialID, []byte(id)), + tlv.New(tagKey, []byte{byte(alg) | byte(kind), byte(digits)}, secret), + props.TagValue(true), + } + + if counter > 0 && kind == HOTP { + tvs = append(tvs, tlv.TagValue{ + Tag: tagInitialCounter, + 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_test.go b/put_test.go new file mode 100644 index 0000000..75db0f1 --- /dev/null +++ b/put_test.go @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestPut(t *testing.T) { + withCard(t, nil, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + err := card.PutOTP("test", secrets.HMACSHA1, secrets.HOTP, 6, []byte{1, 2, 3}, 0, 0) + require.NoError(err) + }) +} + +func TestPutNameTooLong(t *testing.T) { + withCard(t, nil, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + err := card.PutOTP("0123456789012345678901234567890123456789012345678901234567890123456789", secrets.HMACSHA1, secrets.HOTP, 6, []byte{1, 2, 3}, 0, 0) + require.ErrorIs(err, secrets.ErrNameTooLong) + }) +} diff --git a/reset.go b/reset.go new file mode 100644 index 0000000..077e6d4 --- /dev/null +++ b/reset.go @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +// Reset resets the application to just-installed state. +// This command requires no authentication. +// WARNING: This function wipes all secrets on the card. Use with care! +func (c *Card) Reset() error { + _, err := c.send(insReset, 0xde, 0xad) + return err +} diff --git a/reset_test.go b/reset_test.go new file mode 100644 index 0000000..0f2d903 --- /dev/null +++ b/reset_test.go @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestReset(t *testing.T) { + withCard(t, vectorsTOTP[:1], true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + err := card.Reset() + require.NoError(err) + + creds, err := card.List() + require.NoError(err) + require.Len(creds, 0) + }) +} diff --git a/select.go b/select.go new file mode 100644 index 0000000..6f95862 --- /dev/null +++ b/select.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "encoding/binary" + "fmt" + + iso "cunicu.li/go-iso7816" + "cunicu.li/go-iso7816/encoding/tlv" +) + +// Select encapsulates the results of the "SELECT" instruction. +type Select struct { + Version iso.Version + PINCounter uint32 + Salt []byte + Challenge []byte + Algorithm []byte + Serial []byte +} + +func (s *Select) UnmarshalBinary(b []byte) error { + tvs, err := tlv.DecodeSimple(b) + if err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + + for _, tv := range tvs { + switch tv.Tag { + case tagVersion: + if len(tv.Value) < 3 { + return iso.ErrWrongLength + } + + s.Version.Major = int(tv.Value[0]) + s.Version.Minor = int(tv.Value[1]) + s.Version.Patch = int(tv.Value[2]) + + case tagPINCounter: + if len(tv.Value) > 4 { + return iso.ErrWrongLength + } + + buf := []byte{} + for i := 0; i < 4-len(tv.Value); i++ { + buf = append(buf, 0) + } + buf = append(buf, tv.Value...) + + s.PINCounter = binary.BigEndian.Uint32(buf) + + case tagCredentialID: + s.Salt = tv.Value + + case tagChallenge: + s.Challenge = tv.Value + + case tagAlgorithm: + s.Algorithm = tv.Value + + case tagSerialNumber: + s.Serial = tv.Value + + default: + return fmt.Errorf("%w (%#x)", errUnknownTag, tv.Tag) + } + } + + return nil +} + +func (s *Select) HasHealthyPIN() bool { + return s.PINCounter != 0 +} + +func (s *Select) SupportsPasswordStorage() bool { + return !iso.Version{Major: 4, Minor: 11, Patch: 0}.Less(s.Version) +} + +func (s *Select) SupportsExtendedList() bool { + return !iso.Version{Major: 4, Minor: 11, Patch: 0}.Less(s.Version) +} + +func (s *Select) HasEncryptedStorage() bool { + return !iso.Version{Major: 4, Minor: 10, Patch: 0}.Less(s.Version) +} + +func (s *Select) IsOldApplicationVersion() bool { + return s.Version == iso.Version{Major: 0x34, Minor: 0x34, Patch: 0x34} +} + +func (s *Select) RequiresAlwaysPIN() bool { + return s.Version == iso.Version{Major: 4, Minor: 7, Patch: 0} +} + +func (s *Select) SupportsChallengeResponse() bool { + return s.Challenge != nil +} + +func (s *Select) HasActivePIN() bool { + return s.Challenge == nil && s.PINCounter > 0 +} + +// 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{} + if err := s.UnmarshalBinary(resp); err != nil { + return nil, err + } + + c.info = s + + return s, nil +} diff --git a/select_test.go b/select_test.go new file mode 100644 index 0000000..13ce992 --- /dev/null +++ b/select_test.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "testing" + + iso "cunicu.li/go-iso7816" + "github.com/stretchr/testify/assert" + + secrets "cunicu.li/go-trussed-secrets" +) + +func TestSelect(t *testing.T) { + withCard(t, nil, true, func(t *testing.T, card *secrets.Card) { + assert := assert.New(t) + + res, err := card.Select() + assert.NoError(err) + + assert.Empty(res.PINCounter) + assert.Empty(res.Algorithm) + assert.Empty(res.Challenge) + assert.Len(res.Salt, 8) + assert.Len(res.Serial, 4) + assert.Equal(iso.Version{Major: 0x04, Minor: 0x0d, Patch: 0x00}, res.Version) + + t.Logf("select: %+#v", res) + }) +} diff --git a/vectors_test.go b/vectors_test.go new file mode 100644 index 0000000..f9c1caf --- /dev/null +++ b/vectors_test.go @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "encoding/hex" + "time" + + secrets "cunicu.li/go-trussed-secrets" +) + +type vector struct { + ID string + Algorithm secrets.Algorithm + Kind secrets.Kind + Properties secrets.Properties + Digits int + Secret []byte + Time time.Time + Counter uint32 + Code string + Hash []byte +} + +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 +} + +// nolint: gochecknoglobals +var ( + // See: RFC Errata for RFC 6238 (https://www.rfc-editor.org/errata/eid2866) + testSecretSHA1 = fromString("12345678901234567890") + testSecretSHA256 = fromString("12345678901234567890123456789012") + testSecretSHA512 = fromString("1234567890123456789012345678901234567890123456789012345678901234") + + vectorsMix = []vector{ + {ID: "mix-test-01", Algorithm: secrets.HMACSHA1, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA1, Properties: secrets.TouchRequired}, + {ID: "mix-test-02", Algorithm: secrets.HMACSHA256, Kind: secrets.HOTP, Digits: 8, Secret: testSecretSHA256}, + {ID: "mix-test-03", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTPReverse, Digits: 8, Secret: testSecretSHA1}, + {ID: "mix-test-04", Algorithm: secrets.HMACSHA1, Kind: secrets.HMAC, Digits: 6, Secret: testSecretSHA1}, + } + + // RFC 6238 Appendix B - Test Vectors + // + // See: https://datatracker.ietf.org/doc/html/rfc6238#appendix-B + // TODO: SHA512 is not supported by firmware yet (as of 1.7.0) + vectorsTOTP = []vector{ + {ID: "rfc6238-test-01", Algorithm: secrets.HMACSHA1, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(59, 0), Code: "94287082"}, + {ID: "rfc6238-test-02", Algorithm: secrets.HMACSHA256, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(59, 0), Code: "46119246"}, + // {Name: "rfc6238-test-03", Alg: app.HmacSha512, Typ: app.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(59, 0), Code: "90693936"}, + {ID: "rfc6238-test-04", Algorithm: secrets.HMACSHA1, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(1111111109, 0), Code: "07081804"}, + {ID: "rfc6238-test-05", Algorithm: secrets.HMACSHA256, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(1111111109, 0), Code: "68084774"}, + // {Name: "rfc6238-test-06", Alg: app.HmacSha512, Typ: app.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(1111111109, 0), Code: "25091201"}, + {ID: "rfc6238-test-07", Algorithm: secrets.HMACSHA1, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(1111111111, 0), Code: "14050471"}, + {ID: "rfc6238-test-08", Algorithm: secrets.HMACSHA256, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(1111111111, 0), Code: "67062674"}, + // {Name: "rfc6238-test-09", Alg: app.HmacSha512, Typ: app.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(1111111111, 0), Code: "99943326"}, + {ID: "rfc6238-test-10", Algorithm: secrets.HMACSHA1, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(1234567890, 0), Code: "89005924"}, + {ID: "rfc6238-test-11", Algorithm: secrets.HMACSHA256, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(1234567890, 0), Code: "91819424"}, + // {Name: "rfc6238-test-12", Alg: app.HmacSha512, Typ: app.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(1234567890, 0), Code: "93441116"}, + {ID: "rfc6238-test-13", Algorithm: secrets.HMACSHA1, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(2000000000, 0), Code: "69279037"}, + {ID: "rfc6238-test-14", Algorithm: secrets.HMACSHA256, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(2000000000, 0), Code: "90698825"}, + // {Name: "rfc6238-test-15", Alg: app.HmacSha512, Typ: app.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(2000000000, 0), Code: "38618901"}, + {ID: "rfc6238-test-16", Algorithm: secrets.HMACSHA1, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA1, Time: time.Unix(20000000000, 0), Code: "65353130"}, + {ID: "rfc6238-test-17", Algorithm: secrets.HMACSHA256, Kind: secrets.TOTP, Digits: 8, Secret: testSecretSHA256, Time: time.Unix(20000000000, 0), Code: "77737706"}, + // {Name: "rfc6238-test-18", Alg: app.HmacSha512, Typ: app.Totp, Digits: 8, Secret: testSecretSHA512, Time: time.Unix(20000000000, 0), Code: "47863826"}, + } + + // RFC 4226 Appendix D - HOTP Algorithm: Test Values + // + // See: https://datatracker.ietf.org/doc/html/rfc4226#page-32 + vectorsHOTP = []vector{ + {ID: "rfc4226-test-00", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 0, Code: "755224", Hash: fromHex("cc93cf18508d94934c64b65d8ba7667fb7cde4b0")}, + {ID: "rfc4226-test-01", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 1, Code: "287082", Hash: fromHex("75a48a19d4cbe100644e8ac1397eea747a2d33ab")}, + {ID: "rfc4226-test-02", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 2, Code: "359152", Hash: fromHex("0bacb7fa082fef30782211938bc1c5e70416ff44")}, + {ID: "rfc4226-test-03", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 3, Code: "969429", Hash: fromHex("66c28227d03a2d5529262ff016a1e6ef76557ece")}, + {ID: "rfc4226-test-04", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 4, Code: "338314", Hash: fromHex("a904c900a64b35909874b33e61c5938a8e15ed1c")}, + {ID: "rfc4226-test-05", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 5, Code: "254676", Hash: fromHex("a37e783d7b7233c083d4f62926c7a25f238d0316")}, + {ID: "rfc4226-test-06", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 6, Code: "287922", Hash: fromHex("bc9cd28561042c83f219324d3c607256c03272ae")}, + {ID: "rfc4226-test-07", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 7, Code: "162583", Hash: fromHex("a4fb960c0bc06e1eabb804e5b397cdc4b45596fa")}, + {ID: "rfc4226-test-08", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 8, Code: "399871", Hash: fromHex("1b3c89f65e6c9e883012052823443f048b4332db")}, + {ID: "rfc4226-test-09", Algorithm: secrets.HMACSHA1, Kind: secrets.HOTP, Digits: 6, Secret: testSecretSHA1, Counter: 9, Code: "520489", Hash: fromHex("1637409809a679dc698207310c8c7fc07290d9e5")}, + } + + vectors = map[string][]vector{ + "TOTP": vectorsTOTP, + "HOTP": vectorsHOTP, + } +) diff --git a/verify.go b/verify.go new file mode 100644 index 0000000..c7dab7c --- /dev/null +++ b/verify.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets + +import ( + "encoding/binary" + + "cunicu.li/go-iso7816/encoding/tlv" +) + +// VerifyCode verifies incoming OTP codes (aka reverse HOTP). +func (c *Card) VerifyCode(id string, code int) error { + resp := binary.BigEndian.AppendUint32(nil, uint32(code)) + + _, err := c.send(insVerifyCode, 0x00, 0x00, + tlv.New(tagCredentialID, id), + tlv.New(tagResponse, resp), + ) + return err +} diff --git a/verify_test.go b/verify_test.go new file mode 100644 index 0000000..4363199 --- /dev/null +++ b/verify_test.go @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package secrets_test + +import ( + "strconv" + "testing" + + secrets "cunicu.li/go-trussed-secrets" + "github.com/stretchr/testify/require" +) + +func TestVerifyCode(t *testing.T) { + withCard(t, nil, true, func(t *testing.T, card *secrets.Card) { + require := require.New(t) + + err := card.PutOTP("reverse-hotp", secrets.HMACSHA1, secrets.HOTPReverse, 6, testSecretSHA1, 0, 0) + require.NoError(err) + + for _, v := range vectorsHOTP { + code, err := strconv.Atoi(v.Code) + require.NoError(err) + + err = card.VerifyCode("reverse-hotp", code) + require.NoError(err) + } + }) +}