twofactor: linting fixes

This commit is contained in:
2025-11-16 21:51:38 -08:00
parent 3fb2d88a3f
commit f296344acf
14 changed files with 147 additions and 194 deletions

View File

@@ -12,6 +12,12 @@
version: "2" version: "2"
output:
sort-order:
- file
- linter
- severity
issues: issues:
# Maximum count of issues with the same text. # Maximum count of issues with the same text.
# Set to 0 to disable. # Set to 0 to disable.
@@ -454,6 +460,8 @@ linters:
- -QF1008 - -QF1008
# We often explicitly enable old/deprecated ciphers for research. # We often explicitly enable old/deprecated ciphers for research.
- -SA1019 - -SA1019
# Covered by revive.
- -ST1003
usetesting: usetesting:
# Enable/disable `os.TempDir()` detections. # Enable/disable `os.TempDir()` detections.
@@ -472,6 +480,8 @@ linters:
rules: rules:
- path: 'ahash/ahash.go' - path: 'ahash/ahash.go'
linters: [ staticcheck, gosec ] linters: [ staticcheck, gosec ]
- path: 'twofactor/.*.go'
linters: [ exhaustive, mnd, revive ]
- path: 'backoff/backoff_test.go' - path: 'backoff/backoff_test.go'
linters: [ testpackage ] linters: [ testpackage ]
- path: 'dbg/dbg_test.go' - path: 'dbg/dbg_test.go'

View File

@@ -1,5 +1,13 @@
CHANGELOG CHANGELOG
v1.12.4 - 2025-11-16
Changed:
- Linting fixes for twofactor that were previously masked.
v1.12.3 erroneously tagged and pushed
v1.12.2 - 2025-11-16 v1.12.2 - 2025-11-16
Changed: Changed:

View File

@@ -1,42 +0,0 @@
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/2.0/configuration-reference
version: 2.1
# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
jobs:
testbuild:
working_directory: ~/repo
# Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub.
# See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor
docker:
- image: cimg/go:1.22.2
# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps:
- checkout
- restore_cache:
keys:
- go-mod-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: go mod download
- save_cache:
key: go-mod-v4-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
- run:
name: Run tests
command: go test ./...
- run:
name: Run build
command: go build ./...
- store_test_results:
path: /tmp/test-reports
# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
testbuild:
jobs:
- testbuild

View File

@@ -1,4 +1,4 @@
// twofactor implements two-factor authentication. // Package twofactor implements two-factor authentication.
// //
// Currently supported are RFC 4226 HOTP one-time passwords and // Currently supported are RFC 4226 HOTP one-time passwords and
// RFC 6238 TOTP SHA-1 one-time passwords. // RFC 6238 TOTP SHA-1 one-time passwords.

View File

@@ -2,7 +2,7 @@ package twofactor
import ( import (
"crypto" "crypto"
"crypto/sha1" "crypto/sha1" // #nosec G505 - required by RFC
"encoding/base32" "encoding/base32"
"io" "io"
"net/url" "net/url"
@@ -15,11 +15,6 @@ type HOTP struct {
*OATH *OATH
} }
// Type returns OATH_HOTP.
func (otp *HOTP) Type() Type {
return OATH_HOTP
}
// NewHOTP takes the key, the initial counter value, and the number // NewHOTP takes the key, the initial counter value, and the number
// of digits (typically 6 or 8) and returns a new HOTP instance. // of digits (typically 6 or 8) and returns a new HOTP instance.
func NewHOTP(key []byte, counter uint64, digits int) *HOTP { func NewHOTP(key []byte, counter uint64, digits int) *HOTP {
@@ -34,6 +29,11 @@ func NewHOTP(key []byte, counter uint64, digits int) *HOTP {
} }
} }
// Type returns OATH_HOTP.
func (otp *HOTP) Type() Type {
return OATH_HOTP
}
// OTP returns the next OTP and increments the counter. // OTP returns the next OTP and increments the counter.
func (otp *HOTP) OTP() string { func (otp *HOTP) OTP() string {
code := otp.OATH.OTP(otp.counter) code := otp.OATH.OTP(otp.counter)
@@ -79,7 +79,7 @@ func hotpFromURL(u *url.URL) (*HOTP, string, error) {
digits = int(tmpDigits) digits = int(tmpDigits)
} }
var counter uint64 = 0 var counter uint64
if scounter := v.Get("counter"); scounter != "" { if scounter := v.Get("counter"); scounter != "" {
var err error var err error
counter, err = strconv.ParseUint(scounter, 10, 64) counter, err = strconv.ParseUint(scounter, 10, 64)

View File

@@ -1,7 +1,6 @@
package twofactor package twofactor
import ( import (
"fmt"
"testing" "testing"
) )
@@ -25,22 +24,19 @@ var rfcHotpExpected = []string{
// ensures that this implementation is in compliance. // ensures that this implementation is in compliance.
func TestHotpRFC(t *testing.T) { func TestHotpRFC(t *testing.T) {
otp := NewHOTP(rfcHotpKey, 0, 6) otp := NewHOTP(rfcHotpKey, 0, 6)
for i := 0; i < len(rfcHotpExpected); i++ { for i := range rfcHotpExpected {
if otp.Counter() != uint64(i) { if otp.Counter() != uint64(i) {
fmt.Printf("twofactor: invalid counter (should be %d, is %d", t.Fatalf("twofactor: invalid counter (should be %d, is %d",
i, otp.Counter()) i, otp.Counter())
t.FailNow()
} }
code := otp.OTP() code := otp.OTP()
if code == "" { if code == "" {
fmt.Printf("twofactor: failed to produce an OTP\n") t.Fatal("twofactor: failed to produce an OTP")
t.FailNow()
} else if code != rfcHotpExpected[i] { } else if code != rfcHotpExpected[i] {
fmt.Printf("twofactor: invalid OTP\n") t.Logf("twofactor: invalid OTP\n")
fmt.Printf("\tExpected: %s\n", rfcHotpExpected[i]) t.Logf("\tExpected: %s\n", rfcHotpExpected[i])
fmt.Printf("\t Actual: %s\n", code) t.Logf("\t Actual: %s\n", code)
fmt.Printf("\t Counter: %d\n", otp.counter) t.Fatalf("\t Counter: %d\n", otp.counter)
t.FailNow()
} }
} }
} }
@@ -50,15 +46,13 @@ func TestHotpRFC(t *testing.T) {
// expected. // expected.
func TestHotpBadRFC(t *testing.T) { func TestHotpBadRFC(t *testing.T) {
otp := NewHOTP(testKey, 0, 6) otp := NewHOTP(testKey, 0, 6)
for i := 0; i < len(rfcHotpExpected); i++ { for i := range rfcHotpExpected {
code := otp.OTP() code := otp.OTP()
switch code { switch code {
case "": case "":
fmt.Printf("twofactor: failed to produce an OTP\n") t.Error("twofactor: failed to produce an OTP")
t.FailNow()
case rfcHotpExpected[i]: case rfcHotpExpected[i]:
fmt.Printf("twofactor: should not have received a valid OTP\n") t.Error("twofactor: should not have received a valid OTP")
t.FailNow()
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"hash" "hash"
"net/url" "net/url"
"strconv"
"rsc.io/qr" "rsc.io/qr"
) )
@@ -25,12 +26,12 @@ type OATH struct {
} }
// Size returns the output size (in characters) of the password. // Size returns the output size (in characters) of the password.
func (o OATH) Size() int { func (o *OATH) Size() int {
return o.size return o.size
} }
// Counter returns the OATH token's counter. // Counter returns the OATH token's counter.
func (o OATH) Counter() uint64 { func (o *OATH) Counter() uint64 {
return o.counter return o.counter
} }
@@ -40,18 +41,18 @@ func (o *OATH) SetCounter(counter uint64) {
} }
// Key returns the token's secret key. // Key returns the token's secret key.
func (o OATH) Key() []byte { func (o *OATH) Key() []byte {
return o.key[:] return o.key
} }
// Hash returns the token's hash function. // Hash returns the token's hash function.
func (o OATH) Hash() func() hash.Hash { func (o *OATH) Hash() func() hash.Hash {
return o.hash return o.hash
} }
// URL constructs a URL appropriate for the token (i.e. for use in a // URL constructs a URL appropriate for the token (i.e. for use in a
// QR code). // QR code).
func (o OATH) URL(t Type, label string) string { func (o *OATH) URL(t Type, label string) string {
secret := base32.StdEncoding.EncodeToString(o.key) secret := base32.StdEncoding.EncodeToString(o.key)
u := url.URL{} u := url.URL{}
v := url.Values{} v := url.Values{}
@@ -65,10 +66,10 @@ func (o OATH) URL(t Type, label string) string {
u.Path = label u.Path = label
v.Add("secret", secret) v.Add("secret", secret)
if o.Counter() != 0 && t == OATH_HOTP { if o.Counter() != 0 && t == OATH_HOTP {
v.Add("counter", fmt.Sprintf("%d", o.Counter())) v.Add("counter", strconv.FormatUint(o.Counter(), 10))
} }
if o.Size() != defaultSize { if o.Size() != defaultSize {
v.Add("digits", fmt.Sprintf("%d", o.Size())) v.Add("digits", strconv.Itoa(o.Size()))
} }
switch o.algo { switch o.algo {
@@ -84,7 +85,6 @@ func (o OATH) URL(t Type, label string) string {
u.RawQuery = v.Encode() u.RawQuery = v.Encode()
return u.String() return u.String()
} }
var digits = []int64{ var digits = []int64{
@@ -101,10 +101,10 @@ var digits = []int64{
10: 10000000000, 10: 10000000000,
} }
// The top-level type should provide a counter; for example, HOTP // OTP top-level type should provide a counter; for example, HOTP
// will provide the counter directly while TOTP will provide the // will provide the counter directly while TOTP will provide the
// time-stepped counter. // time-stepped counter.
func (o OATH) OTP(counter uint64) string { func (o *OATH) OTP(counter uint64) string {
var ctr [8]byte var ctr [8]byte
binary.BigEndian.PutUint64(ctr[:], counter) binary.BigEndian.PutUint64(ctr[:], counter)
@@ -140,7 +140,7 @@ func truncate(in []byte) int64 {
// QR generates a byte slice containing the a QR code encoded as a // QR generates a byte slice containing the a QR code encoded as a
// PNG with level Q error correction. // PNG with level Q error correction.
func (o OATH) QR(t Type, label string) ([]byte, error) { func (o *OATH) QR(t Type, label string) ([]byte, error) {
u := o.URL(t, label) u := o.URL(t, label)
code, err := qr.Encode(u, qr.Q) code, err := qr.Encode(u, qr.Q)
if err != nil { if err != nil {

View File

@@ -1,7 +1,6 @@
package twofactor package twofactor
import ( import (
"fmt"
"testing" "testing"
) )
@@ -17,14 +16,12 @@ var truncExpect int64 = 0x50ef7f19
// This test runs through the truncation example given in the RFC. // This test runs through the truncation example given in the RFC.
func TestTruncate(t *testing.T) { func TestTruncate(t *testing.T) {
if result := truncate(sha1Hmac); result != truncExpect { if result := truncate(sha1Hmac); result != truncExpect {
fmt.Printf("hotp: expected truncate -> %d, saw %d\n", t.Fatalf("hotp: expected truncate -> %d, saw %d\n",
truncExpect, result) truncExpect, result)
t.FailNow()
} }
sha1Hmac[19]++ sha1Hmac[19]++
if result := truncate(sha1Hmac); result == truncExpect { if result := truncate(sha1Hmac); result == truncExpect {
fmt.Println("hotp: expected truncation to fail") t.Fatal("hotp: expected truncation to fail")
t.FailNow()
} }
} }

View File

@@ -24,7 +24,7 @@ var (
ErrInvalidAlgo = errors.New("twofactor: invalid algorithm") ErrInvalidAlgo = errors.New("twofactor: invalid algorithm")
) )
// Type OTP represents a one-time password token -- whether a // OTP represents a one-time password token -- whether a
// software taken (as in the case of Google Authenticator) or a // software taken (as in the case of Google Authenticator) or a
// hardware token (as in the case of a YubiKey). // hardware token (as in the case of a YubiKey).
type OTP interface { type OTP interface {
@@ -65,8 +65,8 @@ func otpString(otp OTP) string {
} }
// FromURL constructs a new OTP token from a URL string. // FromURL constructs a new OTP token from a URL string.
func FromURL(URL string) (OTP, string, error) { func FromURL(otpURL string) (OTP, string, error) {
u, err := url.Parse(URL) u, err := url.Parse(otpURL)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }

View File

@@ -1,7 +1,6 @@
package twofactor package twofactor
import ( import (
"fmt"
"io" "io"
"testing" "testing"
) )
@@ -10,8 +9,7 @@ func TestHOTPString(t *testing.T) {
hotp := NewHOTP(nil, 0, 6) hotp := NewHOTP(nil, 0, 6)
hotpString := otpString(hotp) hotpString := otpString(hotp)
if hotpString != "OATH-HOTP, 6" { if hotpString != "OATH-HOTP, 6" {
fmt.Println("twofactor: invalid OTP string") t.Fatal("twofactor: invalid OTP string")
t.FailNow()
} }
} }
@@ -23,35 +21,32 @@ func TestURL(t *testing.T) {
otp := NewHOTP(testKey, 0, 6) otp := NewHOTP(testKey, 0, 6)
url := otp.URL("testuser@foo") url := otp.URL("testuser@foo")
otp2, id, err := FromURL(url) otp2, id, err := FromURL(url)
if err != nil { switch {
fmt.Printf("hotp: failed to parse HOTP URL\n") case err != nil:
t.FailNow() t.Fatal("hotp: failed to parse HOTP URL\n")
} else if id != ident { case id != ident:
fmt.Printf("hotp: bad label\n") t.Logf("hotp: bad label\n")
fmt.Printf("\texpected: %s\n", ident) t.Logf("\texpected: %s\n", ident)
fmt.Printf("\t actual: %s\n", id) t.Fatalf("\t actual: %s\n", id)
t.FailNow() case otp2.Counter() != otp.Counter():
} else if otp2.Counter() != otp.Counter() { t.Logf("hotp: OTP counters aren't synced\n")
fmt.Printf("hotp: OTP counters aren't synced\n") t.Logf("\toriginal: %d\n", otp.Counter())
fmt.Printf("\toriginal: %d\n", otp.Counter()) t.Fatalf("\t second: %d\n", otp2.Counter())
fmt.Printf("\t second: %d\n", otp2.Counter())
t.FailNow()
} }
code1 := otp.OTP() code1 := otp.OTP()
code2 := otp2.OTP() code2 := otp2.OTP()
if code1 != code2 { if code1 != code2 {
fmt.Printf("hotp: mismatched OTPs\n") t.Logf("hotp: mismatched OTPs\n")
fmt.Printf("\texpected: %s\n", code1) t.Logf("\texpected: %s\n", code1)
fmt.Printf("\t actual: %s\n", code2) t.Fatalf("\t actual: %s\n", code2)
} }
// There's not much we can do test the QR code, except to // There's not much we can do test the QR code, except to
// ensure it doesn't fail. // ensure it doesn't fail.
_, err = otp.QR(ident) _, err = otp.QR(ident)
if err != nil { if err != nil {
fmt.Printf("hotp: failed to generate QR code PNG (%v)\n", err) t.Fatalf("hotp: failed to generate QR code PNG (%v)\n", err)
t.FailNow()
} }
// This should fail because the maximum size of an alphanumeric // This should fail because the maximum size of an alphanumeric
@@ -63,16 +58,14 @@ func TestURL(t *testing.T) {
var tooBigIdent = make([]byte, 8192) var tooBigIdent = make([]byte, 8192)
_, err = io.ReadFull(PRNG, tooBigIdent) _, err = io.ReadFull(PRNG, tooBigIdent)
if err != nil { if err != nil {
fmt.Printf("hotp: failed to read identity (%v)\n", err) t.Fatalf("hotp: failed to read identity (%v)\n", err)
t.FailNow()
} else if _, err = otp.QR(string(tooBigIdent)); err == nil { } else if _, err = otp.QR(string(tooBigIdent)); err == nil {
fmt.Println("hotp: QR code should fail to encode oversized URL") t.Fatal("hotp: QR code should fail to encode oversized URL")
t.FailNow()
} }
} }
// This test makes sure we can generate codes for padded and non-padded // This test makes sure we can generate codes for padded and non-padded
// entries // entries.
func TestPaddedURL(t *testing.T) { func TestPaddedURL(t *testing.T) {
var urlList = []string{ var urlList = []string{
"otpauth://hotp/?secret=ME", "otpauth://hotp/?secret=ME",
@@ -95,17 +88,15 @@ func TestPaddedURL(t *testing.T) {
for i := range urlList { for i := range urlList {
if o, id, err := FromURL(urlList[i]); err != nil { if o, id, err := FromURL(urlList[i]); err != nil {
fmt.Println("hotp: URL should have parsed successfully (id=", id, ")") t.Log("hotp: URL should have parsed successfully (id=", id, ")")
fmt.Printf("\turl was: %s\n", urlList[i]) t.Logf("\turl was: %s\n", urlList[i])
t.FailNow() t.Fatalf("\t%s, %s\n", o.OTP(), id)
fmt.Printf("\t%s, %s\n", o.OTP(), id)
} else { } else {
code2 := o.OTP() code2 := o.OTP()
if code2 != codeList[i] { if code2 != codeList[i] {
fmt.Printf("hotp: mismatched OTPs\n") t.Logf("hotp: mismatched OTPs\n")
fmt.Printf("\texpected: %s\n", codeList[i]) t.Logf("\texpected: %s\n", codeList[i])
fmt.Printf("\t actual: %s\n", code2) t.Fatalf("\t actual: %s\n", code2)
t.FailNow()
} }
} }
} }
@@ -128,9 +119,8 @@ func TestBadURL(t *testing.T) {
for i := range urlList { for i := range urlList {
if _, _, err := FromURL(urlList[i]); err == nil { if _, _, err := FromURL(urlList[i]); err == nil {
fmt.Println("hotp: URL should not have parsed successfully") t.Log("hotp: URL should not have parsed successfully")
fmt.Printf("\turl was: %s\n", urlList[i]) t.Fatalf("\turl was: %s\n", urlList[i])
t.FailNow()
} }
} }
} }

View File

@@ -2,7 +2,7 @@ package twofactor
import ( import (
"crypto" "crypto"
"crypto/sha1" "crypto/sha1" // #nosec G505 - required by RFC
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/base32" "encoding/base32"
@@ -23,6 +23,42 @@ type TOTP struct {
step uint64 step uint64
} }
// NewTOTP takes a new key, a starting time, a step, the number of
// digits of output (typically 6 or 8) and the hash algorithm to
// use, and builds a new OTP.
func NewTOTP(key []byte, start uint64, step uint64, digits int, algo crypto.Hash) *TOTP {
h := hashFromAlgo(algo)
if h == nil {
return nil
}
return &TOTP{
OATH: &OATH{
key: key,
counter: start,
size: digits,
hash: h,
algo: algo,
},
step: step,
}
}
// NewGoogleTOTP takes a secret as a base32-encoded string and
// returns an appropriate Google Authenticator TOTP instance.
func NewGoogleTOTP(secret string) (*TOTP, error) {
key, err := base32.StdEncoding.DecodeString(secret)
if err != nil {
return nil, err
}
return NewTOTP(key, 0, 30, 6, crypto.SHA1), nil
}
// NewTOTPSHA1 will build a new TOTP using SHA-1.
func NewTOTPSHA1(key []byte, start uint64, step uint64, digits int) *TOTP {
return NewTOTP(key, start, step, digits, crypto.SHA1)
}
// Type returns OATH_TOTP. // Type returns OATH_TOTP.
func (otp *TOTP) Type() Type { func (otp *TOTP) Type() Type {
return OATH_TOTP return OATH_TOTP
@@ -53,34 +89,7 @@ func (otp *TOTP) otpCounter(t uint64) uint64 {
// OTPCounter returns the current time value for the OTP. // OTPCounter returns the current time value for the OTP.
func (otp *TOTP) OTPCounter() uint64 { func (otp *TOTP) OTPCounter() uint64 {
return otp.otpCounter(uint64(timeSource.Now().Unix())) return otp.otpCounter(uint64(timeSource.Now().Unix() & 0x7FFFFFFF)) //#nosec G115 - masked out overflow bits
}
// NewTOTP takes a new key, a starting time, a step, the number of
// digits of output (typically 6 or 8) and the hash algorithm to
// use, and builds a new OTP.
func NewTOTP(key []byte, start uint64, step uint64, digits int, algo crypto.Hash) *TOTP {
h := hashFromAlgo(algo)
if h == nil {
return nil
}
return &TOTP{
OATH: &OATH{
key: key,
counter: start,
size: digits,
hash: h,
algo: algo,
},
step: step,
}
}
// NewTOTPSHA1 will build a new TOTP using SHA-1.
func NewTOTPSHA1(key []byte, start uint64, step uint64, digits int) *TOTP {
return NewTOTP(key, start, step, digits, crypto.SHA1)
} }
func hashFromAlgo(algo crypto.Hash) func() hash.Hash { func hashFromAlgo(algo crypto.Hash) func() hash.Hash {
@@ -105,16 +114,6 @@ func GenerateGoogleTOTP() *TOTP {
return NewTOTP(key, 0, 30, 6, crypto.SHA1) return NewTOTP(key, 0, 30, 6, crypto.SHA1)
} }
// NewGoogleTOTP takes a secret as a base32-encoded string and
// returns an appropriate Google Authenticator TOTP instance.
func NewGoogleTOTP(secret string) (*TOTP, error) {
key, err := base32.StdEncoding.DecodeString(secret)
if err != nil {
return nil, err
}
return NewTOTP(key, 0, 30, 6, crypto.SHA1), nil
}
func totpFromURL(u *url.URL) (*TOTP, string, error) { func totpFromURL(u *url.URL) (*TOTP, string, error) {
label := u.Path[1:] label := u.Path[1:]
v := u.Query() v := u.Query()
@@ -126,11 +125,12 @@ func totpFromURL(u *url.URL) (*TOTP, string, error) {
var algo = crypto.SHA1 var algo = crypto.SHA1
if algorithm := v.Get("algorithm"); algorithm != "" { if algorithm := v.Get("algorithm"); algorithm != "" {
if strings.EqualFold(algorithm, "SHA256") { switch {
case strings.EqualFold(algorithm, "SHA256"):
algo = crypto.SHA256 algo = crypto.SHA256
} else if strings.EqualFold(algorithm, "SHA512") { case strings.EqualFold(algorithm, "SHA512"):
algo = crypto.SHA512 algo = crypto.SHA512
} else if !strings.EqualFold(algorithm, "SHA1") { case !strings.EqualFold(algorithm, "SHA1"):
return nil, "", ErrInvalidAlgo return nil, "", ErrInvalidAlgo
} }
} }

View File

@@ -2,7 +2,6 @@ package twofactor
import ( import (
"crypto" "crypto"
"fmt"
"testing" "testing"
"time" "time"
@@ -14,6 +13,7 @@ var rfcTotpKey = map[crypto.Hash][]byte{
crypto.SHA256: []byte("12345678901234567890123456789012"), crypto.SHA256: []byte("12345678901234567890123456789012"),
crypto.SHA512: []byte("1234567890123456789012345678901234567890123456789012345678901234"), crypto.SHA512: []byte("1234567890123456789012345678901234567890123456789012345678901234"),
} }
var rfcTotpStep uint64 = 30 var rfcTotpStep uint64 = 30
var rfcTotpTests = []struct { var rfcTotpTests = []struct {
@@ -46,17 +46,15 @@ func TestTotpRFC(t *testing.T) {
for _, tc := range rfcTotpTests { for _, tc := range rfcTotpTests {
otp := NewTOTP(rfcTotpKey[tc.Algo], 0, rfcTotpStep, 8, tc.Algo) otp := NewTOTP(rfcTotpKey[tc.Algo], 0, rfcTotpStep, 8, tc.Algo)
if otp.otpCounter(tc.Time) != tc.T { if otp.otpCounter(tc.Time) != tc.T {
fmt.Printf("twofactor: invalid TOTP (t=%d, h=%d)\n", tc.Time, tc.Algo) t.Logf("twofactor: invalid TOTP (t=%d, h=%d)\n", tc.Time, tc.Algo)
fmt.Printf("\texpected: %d\n", tc.T) t.Logf("\texpected: %d\n", tc.T)
fmt.Printf("\t actual: %d\n", otp.otpCounter(tc.Time)) t.Errorf("\t actual: %d\n", otp.otpCounter(tc.Time))
t.Fail()
} }
if code := otp.otp(otp.otpCounter(tc.Time)); code != tc.Code { if code := otp.otp(otp.otpCounter(tc.Time)); code != tc.Code {
fmt.Printf("twofactor: invalid TOTP (t=%d, h=%d)\n", tc.Time, tc.Algo) t.Logf("twofactor: invalid TOTP (t=%d, h=%d)\n", tc.Time, tc.Algo)
fmt.Printf("\texpected: %s\n", tc.Code) t.Logf("\texpected: %s\n", tc.Code)
fmt.Printf("\t actual: %s\n", code) t.Errorf("\t actual: %s\n", code)
t.Fail()
} }
} }
} }

View File

@@ -5,7 +5,7 @@ import (
) )
// Pad calculates the number of '='s to add to our encoded string // Pad calculates the number of '='s to add to our encoded string
// to make base32.StdEncoding.DecodeString happy // to make base32.StdEncoding.DecodeString happy.
func Pad(s string) string { func Pad(s string) string {
if !strings.HasSuffix(s, "=") && len(s)%8 != 0 { if !strings.HasSuffix(s, "=") && len(s)%8 != 0 {
for len(s)%8 != 0 { for len(s)%8 != 0 {

View File

@@ -1,11 +1,12 @@
package twofactor package twofactor_test
import ( import (
"encoding/base32" "encoding/base32"
"fmt"
"math/rand" "math/rand"
"strings" "strings"
"testing" "testing"
"git.wntrmute.dev/kyle/goutils/twofactor"
) )
const letters = "1234567890!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const letters = "1234567890!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -19,34 +20,31 @@ func randString() string {
} }
func TestPadding(t *testing.T) { func TestPadding(t *testing.T) {
for i := 0; i < 300; i++ { for range 300 {
b := randString() b := randString()
origEncoding := string(b) origEncoding := b
modEncoding := strings.ReplaceAll(string(b), "=", "") modEncoding := strings.ReplaceAll(b, "=", "")
str, err := base32.StdEncoding.DecodeString(origEncoding) str, err := base32.StdEncoding.DecodeString(origEncoding)
if err != nil { if err != nil {
fmt.Println("Can't decode: ", string(b)) t.Fatal("Can't decode: ", b)
t.FailNow()
} }
paddedEncoding := Pad(modEncoding) paddedEncoding := twofactor.Pad(modEncoding)
if origEncoding != paddedEncoding { if origEncoding != paddedEncoding {
fmt.Println("Padding failed:") t.Log("Padding failed:")
fmt.Printf("Expected: '%s'", origEncoding) t.Logf("Expected: '%s'", origEncoding)
fmt.Printf("Got: '%s'", paddedEncoding) t.Fatalf("Got: '%s'", paddedEncoding)
t.FailNow()
} else { } else {
mstr, err := base32.StdEncoding.DecodeString(paddedEncoding) var mstr []byte
mstr, err = base32.StdEncoding.DecodeString(paddedEncoding)
if err != nil { if err != nil {
fmt.Println("Can't decode: ", paddedEncoding) t.Fatal("Can't decode: ", paddedEncoding)
t.FailNow()
} }
if string(mstr) != string(str) { if string(mstr) != string(str) {
fmt.Println("Re-padding failed:") t.Log("Re-padding failed:")
fmt.Printf("Expected: '%s'", str) t.Logf("Expected: '%s'", str)
fmt.Printf("Got: '%s'", mstr) t.Fatalf("Got: '%s'", mstr)
t.FailNow()
} }
} }
} }