Skip to content

Commit

Permalink
fix #42 default skew to 1 (#43)
Browse files Browse the repository at this point in the history
* chore: add test to reproduce #42
* feat: add PassCodeCustom and ValidateCustom
* fix #42: set Skew to 1 as default

close #42
  • Loading branch information
KEINOS authored May 1, 2024
1 parent b512b85 commit e663df8
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 84 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func Example() {
// - Algorithm: SHA1
// - Period: 30
// - Secret Size: 128
// - Skew (time tolerance): 0
// - Skew (time tolerance): 1
// - Digits: 6
// * Validation result: Passcode is valid
}
Expand Down
48 changes: 0 additions & 48 deletions totp/common.go

This file was deleted.

106 changes: 95 additions & 11 deletions totp/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log"
"strconv"
"time"

"github.com/KEINOS/go-totp/totp"
)
Expand Down Expand Up @@ -47,7 +48,7 @@ func Example() {
// - Algorithm: SHA1
// - Period: 30
// - Secret Size: 128
// - Skew (time tolerance): 0
// - Skew (time tolerance): 1
// - Digits: 6
// * Validation result: Passcode is valid
}
Expand Down Expand Up @@ -192,7 +193,7 @@ Digits: 8
Issuer: Example.com
Period: 30
Secret Size: 64
Skew: 0
Skew: 1
gX7ff3VlT4sCakCjQH69ZQxTbzs=
-----END TOTP SECRET KEY-----`
Expand All @@ -218,7 +219,7 @@ gX7ff3VlT4sCakCjQH69ZQxTbzs=
// Issuer: Example.com
// Period: 30
// Secret Size: 64
// Skew: 0
// Skew: 1
// Secret: QF7N673VMVHYWATKICRUA7V5MUGFG3Z3
}

Expand Down Expand Up @@ -258,7 +259,7 @@ func ExampleGenKeyFromURI() {
// ============================================================================

func ExampleKey() {
// Generate a new secret key
// Generate a new secret key with default options.
Issuer := "Example.com"
AccountName := "[email protected]"

Expand All @@ -267,12 +268,20 @@ func ExampleKey() {
log.Fatal(err)
}

fmt.Println("Issuer:", key.Options.AccountName)
fmt.Println("AccountName:", key.Options.AccountName)
// Generate 6 digits passcode (valid for 30 seconds)
// For generating a passcode for a custom time, use PassCodeCustom() method.
passCode, err := key.PassCode()
if err != nil {
log.Fatal(err)
}

// Validate the passcode
if key.Validate(passCode) {
fmt.Println("Given passcode is valid")
}
//
// Output:
// Issuer: [email protected]
// AccountName: [email protected]
// Given passcode is valid
}

// In this example, we will re-generate/recover a new Key object from a backed-up
Expand Down Expand Up @@ -323,12 +332,87 @@ func ExampleKey_regenerate() {
// Issuer: Example.com
// Period: 30
// Secret Size: 20
// Skew: 0
// Skew: 1
//
// gX7ff3VlT4sCakCjQH69ZQxTbzs=
// -----END TOTP SECRET KEY-----
}

func ExampleKey_PassCode() {
// Generate a new secret key
Issuer := "Example.com"
AccountName := "[email protected]"

key, err := totp.GenerateKey(Issuer, AccountName)
if err != nil {
log.Fatal(err)
}

// Generate 6 digits passcode (valid for 30 seconds)
code, err := key.PassCode()
if err != nil {
log.Fatal(err)
}

// Validate the passcode
if key.Validate(code) {
fmt.Println("Passcode is valid with current time")
}

// Validate the passcode with a custom time
validationTime := time.Now().Add(-300 * time.Second)

if key.ValidateCustom(code, validationTime) {
fmt.Println("Passcode is valid with custom time")
} else {
fmt.Println("Passcode is invalid with custom time")
}
//
// Output:
// Passcode is valid with current time
// Passcode is invalid with custom time
}

func ExampleKey_PassCodeCustom() {
// Generate a new secret key
Issuer := "Example.com"
AccountName := "[email protected]"

key, err := totp.GenerateKey(Issuer, AccountName)
if err != nil {
log.Fatal(err)
}

timeNow := time.Now()

// Generate a passcode for a specific time (300 seconds ago)
code, err := key.PassCodeCustom(timeNow.Add(-300 * time.Second))
if err != nil {
log.Fatal(err)
}

// Validating with the current time should fail
if key.Validate(code) {
fmt.Println("Passcode is valid with current time")
} else {
fmt.Println("Passcode is invalid with current time")
}

// To validate a passcode for a specific time, use ValidateCustom()
// method.
validationTime := timeNow.Add(-300 * time.Second)

if key.ValidateCustom(code, validationTime) {
fmt.Println("Passcode is valid with custom time")
} else {
fmt.Println("Passcode is invalid with custom time")
}
//
// Output:
// Passcode is invalid with current time
// Passcode is valid with custom time
}

func ExampleKey_PEM() {
origin := "otpauth://totp/Example.com:[email protected]?algorithm=SHA1&" +
"digits=6&issuer=Example.com&period=30&secret=QF7N673VMVHYWATKICRUA7V5MUGFG3Z3"
Expand All @@ -353,7 +437,7 @@ func ExampleKey_PEM() {
// Issuer: Example.com
// Period: 30
// Secret Size: 20
// Skew: 0
// Skew: 1
//
// gX7ff3VlT4sCakCjQH69ZQxTbzs=
// -----END TOTP SECRET KEY-----
Expand Down Expand Up @@ -573,7 +657,7 @@ func ExampleOptions() {
// Digits: 6
// Period: 30
// Secret Size: 128
// Skew: 0
// Skew: 1
}

// ============================================================================
Expand Down
33 changes: 32 additions & 1 deletion totp/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ func (k *Key) PassCode() (string, error) {
)
}

// PassCodeCustom is similar to PassCode() but allows you to specify the time
// to generate the passcode.
func (k *Key) PassCodeCustom(genTime time.Time) (string, error) {
//nolint:wrapcheck // we won't wrap the error here
return totp.GenerateCodeCustom(
k.Secret.Base32(),
genTime.UTC(),
totp.ValidateOpts{
Period: k.Options.Period,
Skew: k.Options.Skew,
Digits: k.Options.Digits.OTPDigits(),
Algorithm: k.Options.Algorithm.OTPAlgorithm(),
},
)
}

// PEM returns the key in PEM formatted string.
func (k *Key) PEM() (string, error) {
out := pemEncodeToMemory(&pem.Block{
Expand Down Expand Up @@ -284,6 +300,21 @@ func (k *Key) URI() string {
}

// Validate returns true if the given passcode is valid for the current time.
// For custom time, use ValidateCustom() instead.
func (k *Key) Validate(passcode string) bool {
return Validate(passcode, k.Secret.Base32(), k.Options)
return Validate(
passcode,
k.Secret.Base32(),
k.Options,
)
}

// ValidateCustom returns true if the given passcode is valid for the custom time.
func (k *Key) ValidateCustom(passcode string, validationTime time.Time) bool {
return ValidateCustom(
passcode,
k.Secret.Base32(),
validationTime.UTC(),
k.Options,
)
}
39 changes: 39 additions & 0 deletions totp/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/rand"
"encoding/pem"
"testing"
"time"

"github.com/pkg/errors"
origOtp "github.com/pquerna/otp"
Expand Down Expand Up @@ -291,3 +292,41 @@ func TestKey_PEM(t *testing.T) {
require.Contains(t, err.Error(), "failed to encode key to PEM")
require.Empty(t, pemOut)
}

// ============================================================================
// Tests for fixed issues
// ============================================================================
// These tests reproduce the issues and should pass if the issue is fixed.

// Issue #42.
// If `Period` is set short with `Skew=0`, the passcode validation often fails.
func TestKey_skew_as_one(t *testing.T) {
t.Parallel()

key, err := GenerateKey("dummy issuer", "dummy account")
require.NoError(t, err, "failed to generate TOTP key during test setup")

key.Options.Period = 3 // set short to 3 seconds
timeSleep := key.Options.Period - 1 // sleep almost last-minute
numValid := 0
numIterations := 10

// If skew is set to 0, the validation fails 60-70% of the time.
for range numIterations {
passCode, err := key.PassCode()
require.NoError(t, err, "failed to generate passcode")

// Sleep to validate passcode with an almost last-minute deadline.
time.Sleep(time.Second * time.Duration(timeSleep))

if ok := key.Validate(passCode); ok {
numValid++
}
}

expect := numIterations
actual := numValid

require.Equal(t, expect, actual,
"not all generated passcodes are valid")
}
10 changes: 5 additions & 5 deletions totp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const (
OptionDigitsDefault = Digits(6) // Google Authenticator does not work other than 6 digits.
OptionPeriodDefault = uint(30) // 30 seconds is recommended in RFC-6238.
OptionSecretSizeDefault = uint(128) // 128 Bytes.
OptionSkewDefault = uint(0) // ± Periods. No tolerance.
OptionSkewDefault = uint(1) // ± 1 period of tolerance.
)

// ============================================================================
Expand Down Expand Up @@ -49,9 +49,10 @@ type Options struct {
Period uint
// SecretSize is the size of the generated Secret. (Default: 128 bytes)
SecretSize uint
// Skew is the periods before or after the current time to allow.
// Skew is the periods before or after the current time to allow. (Default: 1)
//
// Value of 1 allows up to Period of either side of the specified time.
// Defaults to 0 allowed skews. Values greater than 1 are likely sketchy.
// Values greater than 1 are likely sketchy.
Skew uint
}

Expand Down Expand Up @@ -97,8 +98,7 @@ func (opts *Options) SetDefault() {
opts.Digits = OptionDigitsDefault
}

// This is redundant, but it's here to make sure that the default value is
// always set.
// Fix #42
if opts.Skew == 0 {
opts.Skew = OptionSkewDefault
}
Expand Down
22 changes: 4 additions & 18 deletions totp/qrcode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"image/png"
"io"
"testing"
"time"

"github.com/boombuler/barcode"
"github.com/boombuler/barcode/qr"
Expand Down Expand Up @@ -40,24 +39,11 @@ func TestQRCode_PNG_golden(t *testing.T) {
require.Equal(t, pngImg1, pngImg2, "the generated PNG images should be the same")

// Validate passcode in-time
{
passcode, err := key.PassCode()
require.NoError(t, err, "failed to generate passcode during test")
passcode, err := key.PassCode()
require.NoError(t, err, "failed to generate passcode during test")

ok := key.Validate(passcode)
require.True(t, ok, "the passcode should be valid")
}

// Validate passcode outdated
{
passcode, err := key.PassCode()
require.NoError(t, err, "failed to generate passcode during test")

time.Sleep(3 * time.Second) // let the passcode be expired

ok := key.Validate(passcode)
require.False(t, ok, "expired passcode should be invalid")
}
ok := key.Validate(passcode)
require.True(t, ok, "the passcode should be valid")
}

func TestQRCode_PNG_empty_uri(t *testing.T) {
Expand Down
Loading

0 comments on commit e663df8

Please sign in to comment.