Skip to content

Commit

Permalink
Added functions "GenerateCode", "GenerateCodeCustom" to hotp & totp.
Browse files Browse the repository at this point in the history
  • Loading branch information
Cathal Garvey committed Apr 8, 2016
1 parent e1e5900 commit c68d2e3
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 38 deletions.
58 changes: 39 additions & 19 deletions hotp/hotp.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,27 +57,23 @@ type ValidateOpts struct {
Algorithm otp.Algorithm
}

// ValidateCustom validates an HOTP with customizable options. Most users should
// use Validate().
func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
passcode = strings.TrimSpace(passcode)

switch opts.Digits {
case otp.DigitsSix:
if len(passcode) != 6 {
return false, otp.ErrValidateInputInvalidLength6
}
case otp.DigitsEight:
if len(passcode) != 8 {
return false, otp.ErrValidateInputInvalidLength8
}
default:
panic("unsupported Digits value.")
}
// GenerateCode creates a HOTP passcode given a counter and secret.
// This is a shortcut for GenerateCodeCustom, with parameters that
// are compataible with Google-Authenticator.
func GenerateCode(secret string, counter uint64) string {
code, _ := GenerateCodeCustom(secret, counter, ValidateOpts{
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
})
return code
}

// GenerateCodeCustom uses a counter and secret value and options struct to
// create a passcode.
func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) {
secretBytes, err := base32.StdEncoding.DecodeString(secret)
if err != nil {
return false, otp.ErrValidateSecretInvalidBase32
return "", otp.ErrValidateSecretInvalidBase32
}

buf := make([]byte, 8)
Expand Down Expand Up @@ -108,7 +104,31 @@ func ValidateCustom(passcode string, counter uint64, secret string, opts Validat
fmt.Printf("mod'ed=%v\n", mod)
}

otpstr := opts.Digits.Format(mod)
return opts.Digits.Format(mod), nil
}

// ValidateCustom validates an HOTP with customizable options. Most users should
// use Validate().
func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
passcode = strings.TrimSpace(passcode)

switch opts.Digits {
case otp.DigitsSix:
if len(passcode) != 6 {
return false, otp.ErrValidateInputInvalidLength6
}
case otp.DigitsEight:
if len(passcode) != 8 {
return false, otp.ErrValidateInputInvalidLength8
}
default:
panic("unsupported Digits value.")
}

otpstr, err := GenerateCodeCustom(secret, counter, opts)
if err != nil {
return false, err
}

if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 {
return true, nil
Expand Down
27 changes: 22 additions & 5 deletions hotp/hotp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package hotp

import (
"github.com/pquerna/otp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"encoding/base32"
Expand All @@ -32,11 +33,10 @@ type tc struct {
Secret string
}

// Test values from http://tools.ietf.org/html/rfc4226#appendix-D
func TestValidateRFCMatrix(t *testing.T) {
secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))
var (
secSha1 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))

tests := []tc{
rfcMatrixTCs = []tc{
tc{0, "755224", otp.AlgorithmSHA1, secSha1},
tc{1, "287082", otp.AlgorithmSHA1, secSha1},
tc{2, "359152", otp.AlgorithmSHA1, secSha1},
Expand All @@ -49,7 +49,12 @@ func TestValidateRFCMatrix(t *testing.T) {
tc{9, "520489", otp.AlgorithmSHA1, secSha1},
}

for _, tx := range tests {
)

// Test values from http://tools.ietf.org/html/rfc4226#appendix-D
func TestValidateRFCMatrix(t *testing.T) {

for _, tx := range rfcMatrixTCs {
valid, err := ValidateCustom(tx.TOTP, tx.Counter, tx.Secret,
ValidateOpts{
Digits: otp.DigitsSix,
Expand All @@ -62,6 +67,18 @@ func TestValidateRFCMatrix(t *testing.T) {
}
}

func TestGenerateRFCMatrix(t *testing.T) {
for _, tx := range rfcMatrixTCs {
passcode, err := GenerateCodeCustom(tx.Secret, tx.Counter,
ValidateOpts{
Digits: otp.DigitsSix,
Algorithm: tx.Mode,
})
assert.Nil(t, err)
assert.Equal(t, tx.TOTP, passcode)
}
}

func TestValidateInvalid(t *testing.T) {
secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))

Expand Down
31 changes: 31 additions & 0 deletions totp/totp.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ func Validate(passcode string, secret string) bool {
return rv
}

// GenerateCode creates a TOTP token using the current time.
// A shortcut for GenerateCodeCustom, GenerateCode uses a configuration
// that is compatible with Google-Authenticator and most clients.
func GenerateCode(secret string, t time.Time) string {
code, _ := GenerateCodeCustom(secret, t, ValidateOpts{
Period: 30,
Skew: 1,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
})
return code
}

// ValidateOpts provides options for ValidateCustom().
type ValidateOpts struct {
// Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
Expand All @@ -61,6 +74,24 @@ type ValidateOpts struct {
Algorithm otp.Algorithm
}

// GenerateCodeCustom takes a timepoint and produces a passcode using a
// secret and the provided opts. (Under the hood, this is making an adapted
// call to hotp.GenerateCodeCustom)
func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) {
if opts.Period == 0 {
opts.Period = 30
}
counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{
Digits: opts.Digits,
Algorithm: opts.Algorithm,
})
if err != nil {
return "", err
}
return passcode, nil
}

// ValidateCustom validates a TOTP given a user specified time and custom options.
// Most users should use Validate() to provide an interpolatable TOTP experience.
func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) {
Expand Down
43 changes: 29 additions & 14 deletions totp/totp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package totp

import (
"github.com/pquerna/otp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"encoding/base32"
Expand All @@ -33,20 +34,12 @@ type tc struct {
Secret string
}

//
// Test vectors from http://tools.ietf.org/html/rfc6238#appendix-B
// NOTE -- the test vectors are documented as having the SAME
// secret -- this is WRONG -- they have a variable secret
// depending upon the hmac algorithm:
// http://www.rfc-editor.org/errata_search.php?rfc=6238
// this only took a few hours of head/desk interaction to figure out.
//
func TestValidateRFCMatrix(t *testing.T) {
secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))
secSha256 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890123456789012"))
secSha512 := base32.StdEncoding.EncodeToString([]byte("1234567890123456789012345678901234567890123456789012345678901234"))
var (
secSha1 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))
secSha256 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890123456789012"))
secSha512 = base32.StdEncoding.EncodeToString([]byte("1234567890123456789012345678901234567890123456789012345678901234"))

tests := []tc{
rfcMatrixTCs = []tc{
tc{59, "94287082", otp.AlgorithmSHA1, secSha1},
tc{59, "46119246", otp.AlgorithmSHA256, secSha256},
tc{59, "90693936", otp.AlgorithmSHA512, secSha512},
Expand All @@ -66,8 +59,18 @@ func TestValidateRFCMatrix(t *testing.T) {
tc{20000000000, "77737706", otp.AlgorithmSHA256, secSha256},
tc{20000000000, "47863826", otp.AlgorithmSHA512, secSha512},
}
)

for _, tx := range tests {
//
// Test vectors from http://tools.ietf.org/html/rfc6238#appendix-B
// NOTE -- the test vectors are documented as having the SAME
// secret -- this is WRONG -- they have a variable secret
// depending upon the hmac algorithm:
// http://www.rfc-editor.org/errata_search.php?rfc=6238
// this only took a few hours of head/desk interaction to figure out.
//
func TestValidateRFCMatrix(t *testing.T) {
for _, tx := range rfcMatrixTCs {
valid, err := ValidateCustom(tx.TOTP, tx.Secret, time.Unix(tx.TS, 0).UTC(),
ValidateOpts{
Digits: otp.DigitsEight,
Expand All @@ -80,6 +83,18 @@ func TestValidateRFCMatrix(t *testing.T) {
}
}

func TestGenerateRFCTCs(t *testing.T) {
for _, tx := range rfcMatrixTCs {
passcode, err := GenerateCodeCustom(tx.Secret, time.Unix(tx.TS, 0).UTC(),
ValidateOpts{
Digits: otp.DigitsEight,
Algorithm: tx.Mode,
})
assert.Nil(t, err)
assert.Equal(t, tx.TOTP, passcode)
}
}

func TestValidateSkew(t *testing.T) {
secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890"))

Expand Down

0 comments on commit c68d2e3

Please sign in to comment.