diff --git a/.gitignore b/.gitignore index 0026861..379c093 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,2 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - +*.test *.exe diff --git a/README.md b/README.md index 5607e99..d7efdb4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ import "onetime" var secret = []byte("SOME_SECRET") var counter = 123456 -var otp = onetime.Simple(6) +var otp, _ = onetime.Simple(6) var code = otp.HOTP(secret, counter) ``` @@ -23,7 +23,7 @@ Google authenticator style 8-digit TOTP code: import "onetime" var secret = []byte("SOME_SECRET") -var otp = onetime.Simple(8) +var otp, _ = onetime.Simple(8) var code = otp.TOTP(secret) ``` @@ -36,16 +36,16 @@ import ( ) var secret = []byte("SOME_SECRET") -var ts, _ = time.ParseDuration("5s") +const ts = 5 * time.Second var t = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) -var otp = onetime.OneTimePassword{Digit: 9, TimeStep: ts, BaseTime: t, Hash: sha256.New} +var otp = onetime.OneTimePassword{Digit: 9, TimeStep: ts, BaseTime: t, Hash: sha256.New} var code = otp.TOTP(secret) ``` Documentation ------------- -Package doc can be found [at pkgdoc.org](http://go.pkgdoc.org/github.com/gwwfps/onetime). +Package doc can be found [at godoc.org](http://godoc.org/github.com/gwwfps/onetime). License ------- -This library is released under a simplified BSD license. \ No newline at end of file +This library is released under a simplified BSD license. diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..fe8ac24 --- /dev/null +++ b/example_test.go @@ -0,0 +1,39 @@ +package onetime + +import ( + "crypto/sha256" + "fmt" + "time" + + "github.com/gwwfps/onetime" +) + +func Example_simple() { + // Simple 6-digit HOTP code: + var secret = []byte("SOME_SECRET") + var counter uint64 = 123456 + var otp, _ = onetime.Simple(6) + var code = otp.HOTP(secret, counter) + fmt.Println(code) + // Output: + // 260040 +} + +func Example_authenticator() { + // Google authenticator style 8-digit TOTP code: + var secret = []byte("SOME_SECRET") + var otp, _ = onetime.Simple(8) + var code = otp.TOTP(secret) + fmt.Println(code) +} + +func Example_custom() { + // 9-digit 5-second-step TOTP starting on midnight 2000-01-01 UTC, using SHA-256: + + var secret = []byte("SOME_SECRET") + const ts = 5 * time.Second + var t = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) + var otp = onetime.OneTimePassword{Digit: 9, TimeStep: ts, BaseTime: t, Hash: sha256.New} + var code = otp.TOTP(secret) + fmt.Println(code) +} diff --git a/onetime.go b/onetime.go index 90af1f7..06e9fad 100644 --- a/onetime.go +++ b/onetime.go @@ -1,76 +1,74 @@ -/* - Package onetime provides a library for one-time password generation, - implementing the HOTP and TOTP algorithms as specified by IETF RFC-4226 - and RFC-6238. -*/ +// Package onetime provides a library for one-time password generation, +// implementing the HOTP and TOTP algorithms as specified by IETF RFC-4226 +// and RFC-6238. package onetime import ( - "crypto/hmac" - "crypto/sha1" - "encoding/binary" - "errors" - "hash" - "math" - "time" + "crypto/hmac" + "crypto/sha1" + "encoding/binary" + "errors" + "hash" + "math" + "time" ) // OneTimePassword stores the configuration values relevant to HOTP/TOTP calculations. type OneTimePassword struct { - Digit int // Length of code generated - TimeStep time.Duration // Length of each time step for TOTP - BaseTime time.Time // The start time for TOTP step calculation - Hash func() hash.Hash // Hash algorithm used with HMAC + Digit int // Length of code generated + TimeStep time.Duration // Length of each time step for TOTP + BaseTime time.Time // The start time for TOTP step calculation + Hash func() hash.Hash // Hash algorithm used with HMAC } // HOTP returns a HOTP code with the given secret and counter. func (otp *OneTimePassword) HOTP(secret []byte, count uint64) uint { - hs := otp.hmacSum(secret, count) - return otp.truncate(hs) + hs := otp.hmacSum(secret, count) + return otp.truncate(hs) } func (otp *OneTimePassword) hmacSum(secret []byte, count uint64) []byte { - mac := hmac.New(otp.Hash, secret) - binary.Write(mac, binary.BigEndian, count) - return mac.Sum(nil) + mac := hmac.New(otp.Hash, secret) + binary.Write(mac, binary.BigEndian, count) + return mac.Sum(nil) } func (otp *OneTimePassword) truncate(hs []byte) uint { - sbits := dt(hs) - snum := uint(sbits[3]) | uint(sbits[2])<<8 - snum |= uint(sbits[1])<<16 | uint(sbits[0])<<24 - return snum % uint(math.Pow(10, float64(otp.Digit))) + sbits := dt(hs) + snum := uint(sbits[3]) | uint(sbits[2])<<8 + snum |= uint(sbits[1])<<16 | uint(sbits[0])<<24 + return snum % uint(math.Pow(10, float64(otp.Digit))) } // Simple returns a new OneTimePassword with the specified HTOP code length, // SHA-1 as the HMAC hash algorithm, the Unix epoch as the base time, and // 30 seconds as the step length. func Simple(digit int) (otp OneTimePassword, err error) { - if digit < 6 { - err = errors.New("A minimum of 6 digits is required for a valid HTOP code.") - return - } else if digit > 9 { - err = errors.New("An HTOP code cannot be longer than 9 digits.") - return - } - step, _ := time.ParseDuration("30s") - otp = OneTimePassword{digit, step, time.Unix(0, 0), sha1.New} - return + if digit < 6 { + err = errors.New("minimum of 6 digits is required for a valid HTOP code") + return + } else if digit > 9 { + err = errors.New("HTOP code cannot be longer than 9 digits") + return + } + const step = 30 * time.Second + otp = OneTimePassword{digit, step, time.Unix(0, 0), sha1.New} + return } // TOTP returns a TOTP code calculated with the current time and the given secret. func (otp *OneTimePassword) TOTP(secret []byte) uint { - return otp.HOTP(secret, otp.steps(time.Now())) + return otp.HOTP(secret, otp.steps(time.Now())) } func (otp *OneTimePassword) steps(now time.Time) uint64 { - elapsed := now.Unix() - otp.BaseTime.Unix() - return uint64(math.Floor(float64(elapsed) / otp.TimeStep.Seconds())) + elapsed := now.Unix() - otp.BaseTime.Unix() + return uint64(float64(elapsed) / otp.TimeStep.Seconds()) } func dt(hs []byte) []byte { - offset := int(hs[len(hs)-1] & 0xf) - p := hs[offset : offset+4] - p[0] &= 0x7f - return p + offset := int(hs[len(hs)-1] & 0xf) + p := hs[offset : offset+4] + p[0] &= 0x7f + return p } diff --git a/onetime_test.go b/onetime_test.go index df00d67..aa7b899 100644 --- a/onetime_test.go +++ b/onetime_test.go @@ -1,95 +1,107 @@ package onetime import ( - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "strings" - "testing" - "time" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "strings" + "testing" + "time" ) -// TestHOTP validates the HOTP implementation against test values provided in +// TestHOTP validates the HOTP implementation against test values provided in // Appendix D of RFC-4226. func TestHOTP(t *testing.T) { - expected := []uint{ - 755224, - 287082, - 359152, - 969429, - 338314, - 254676, - 287922, - 162583, - 399871, - 520489, - } - secret := []byte("12345678901234567890") + expected := []uint{ + 755224, + 287082, + 359152, + 969429, + 338314, + 254676, + 287922, + 162583, + 399871, + 520489, + } + secret := []byte("12345678901234567890") - otp, _ := Simple(6) + otp, _ := Simple(6) - for i, exp := range expected { - if v := otp.HOTP(secret, uint64(i)); v != exp { - t.Errorf("HOTP(secret, %v) = %v, want %v", i, v, exp) - } - } + for i, exp := range expected { + if v := otp.HOTP(secret, uint64(i)); v != exp { + t.Errorf("HOTP(secret, %v) = %v, want %v", i, v, exp) + } + } } -// TestTOTP validates the TOTP implementation against test values provided in +// TestTOTP validates the TOTP implementation against test values provided in // Appendix B of RFC-6238. func TestTOTP(t *testing.T) { - expected := []uint{ - 94287082, - 46119246, - 90693936, - 7081804, - 68084774, - 25091201, - 14050471, - 67062674, - 99943326, - 89005924, - 91819424, - 93441116, - 69279037, - 90698825, - 38618901, - 65353130, - 77737706, - 47863826, - } + expected := []uint{ + 94287082, + 46119246, + 90693936, + 7081804, + 68084774, + 25091201, + 14050471, + 67062674, + 99943326, + 89005924, + 91819424, + 93441116, + 69279037, + 90698825, + 38618901, + 65353130, + 77737706, + 47863826, + } - times := []time.Time{ - time.Unix(59, 0), - time.Unix(1111111109, 0), - time.Unix(1111111111, 0), - time.Unix(1234567890, 0), - time.Unix(2000000000, 0), - time.Unix(20000000000, 0), - } + times := []time.Time{ + time.Unix(59, 0), + time.Unix(1111111109, 0), + time.Unix(1111111111, 0), + time.Unix(1234567890, 0), + time.Unix(2000000000, 0), + time.Unix(20000000000, 0), + } - digit := 8 - step, _ := time.ParseDuration("30s") - otps := []OneTimePassword{ - OneTimePassword{digit, step, time.Unix(0, 0), sha1.New}, - OneTimePassword{digit, step, time.Unix(0, 0), sha256.New}, - OneTimePassword{digit, step, time.Unix(0, 0), sha512.New}, - } + digit := 8 + const step = 30 * time.Second + otps := []OneTimePassword{ + OneTimePassword{digit, step, time.Unix(0, 0), sha1.New}, + OneTimePassword{digit, step, time.Unix(0, 0), sha256.New}, + OneTimePassword{digit, step, time.Unix(0, 0), sha512.New}, + } - keyPart := "1234567890" - secrets := [][]byte{ - []byte(strings.Repeat(keyPart, 2)), - []byte(strings.Repeat(keyPart, 4)[:32]), - []byte(strings.Repeat(keyPart, 8)[:64]), - } + keyPart := "1234567890" + secrets := [][]byte{ + []byte(strings.Repeat(keyPart, 2)), + []byte(strings.Repeat(keyPart, 4)[:32]), + []byte(strings.Repeat(keyPart, 8)[:64]), + } - for i, exp := range expected { - otp := otps[i%3] - secret := secrets[i%3] - now := times[i/3] - if v := otp.HOTP(secret, otp.steps(now)); v != exp { - t.Errorf("%s", uint64(now.Unix()-otp.BaseTime.Unix())) - t.Errorf("TOTP(secret) = %v, want %v (time: %v, hash: %v)", v, exp, now, otp.Hash) - } - } + for i, exp := range expected { + otp := otps[i%3] + secret := secrets[i%3] + now := times[i/3] + if v := otp.HOTP(secret, otp.steps(now)); v != exp { + t.Error("time:", uint64(now.Unix()-otp.BaseTime.Unix())) + t.Errorf("TOTP(secret) = %v, want %v (time: %v, hash: %v)", v, exp, now, otp.Hash) + } + } +} + +func BenchmarkHOTPsha256(b *testing.B) { + epoch := time.Unix(0, 0) + otp := OneTimePassword{8, 30 * time.Second, epoch, sha256.New} + keyPart := "1234567890" + secret := []byte(strings.Repeat(keyPart, 4)) + now := time.Unix(20000000000, 0) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = otp.HOTP(secret, otp.steps(now)) + } }