twofactor: linting fixes
This commit is contained in:
@@ -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'
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user