From 1dec15fd11a521cb8d0793109d36a0699031412b Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 19 Dec 2013 00:21:26 -0700 Subject: [PATCH] add missing files new files are oath_test totp code --- oath_test.go | 30 ++++++++++ totp.go | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++ totp_test.go | 55 +++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 oath_test.go create mode 100644 totp.go create mode 100644 totp_test.go diff --git a/oath_test.go b/oath_test.go new file mode 100644 index 0000000..7b145b0 --- /dev/null +++ b/oath_test.go @@ -0,0 +1,30 @@ +package twofactor + +import ( + "fmt" + "testing" +) + +var sha1Hmac = []byte{ + 0x1f, 0x86, 0x98, 0x69, 0x0e, + 0x02, 0xca, 0x16, 0x61, 0x85, + 0x50, 0xef, 0x7f, 0x19, 0xda, + 0x8e, 0x94, 0x5b, 0x55, 0x5a, +} + +var truncExpect int64 = 0x50ef7f19 + +// This test runs through the truncation example given in the RFC. +func TestTruncate(t *testing.T) { + if result := truncate(sha1Hmac); result != truncExpect { + fmt.Printf("hotp: expected truncate -> %d, saw %d\n", + truncExpect, result) + t.FailNow() + } + + sha1Hmac[19]++ + if result := truncate(sha1Hmac); result == truncExpect { + fmt.Println("hotp: expected truncation to fail") + t.FailNow() + } +} diff --git a/totp.go b/totp.go new file mode 100644 index 0000000..f475212 --- /dev/null +++ b/totp.go @@ -0,0 +1,151 @@ +package twofactor + +import ( + "crypto" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base32" + "hash" + "io" + "net/url" + "strconv" + "time" +) + +type TOTP struct { + *oath + step uint64 +} + +func (otp *TOTP) Type() Type { + return OATH_TOTP +} + +func (otp *TOTP) otp(counter uint64) string { + return otp.oath.OTP(counter) +} + +func (otp *TOTP) OTP() string { + return otp.otp(otp.OTPCounter()) +} + +func (otp *TOTP) URL(label string) string { + return otp.oath.URL(otp.Type(), label) +} + +func (otp *TOTP) SetProvider(provider string) { + otp.provider = provider +} + +func (otp *TOTP) otpCounter(t uint64) uint64 { + return (t - otp.counter) / otp.step +} + +func (otp *TOTP) OTPCounter() uint64 { + return otp.otpCounter(uint64(time.Now().Unix())) +} + +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, + } + +} + +func NewTOTPSHA1(key []byte, start uint64, step uint64, digits int) *TOTP { + return NewTOTP(key, start, step, digits, crypto.SHA1) +} + +func NewTOTPSHA256(key []byte, start uint64, step uint64, digits int) *TOTP { + return NewTOTP(key, start, step, digits, crypto.SHA256) +} + +func NewTOTPSHA512(key []byte, start uint64, step uint64, digits int) *TOTP { + return NewTOTP(key, start, step, digits, crypto.SHA512) +} + +func hashFromAlgo(algo crypto.Hash) func() hash.Hash { + switch algo { + case crypto.SHA1: + return sha1.New + case crypto.SHA256: + return sha256.New + case crypto.SHA512: + return sha512.New + } + return nil +} + +// GenerateGoogleTOTP produces a new TOTP token with the defaults expected by +// Google Authenticator. +func GenerateGoogleTOTP() *TOTP { + key := make([]byte, sha1.Size) + if _, err := io.ReadFull(PRNG, key); err != nil { + return nil + } + return NewTOTP(key, 0, 30, 6, crypto.SHA1) +} + +func totpFromURL(u *url.URL) (*TOTP, string, error) { + label := u.Path[1:] + v := u.Query() + + secret := v.Get("secret") + if secret == "" { + return nil, "", ErrInvalidURL + } + + var algo = crypto.SHA1 + if algorithm := v.Get("algorithm"); algorithm != "" { + switch { + case algorithm == "SHA256": + algo = crypto.SHA256 + case algorithm == "SHA512": + algo = crypto.SHA512 + case algorithm != "SHA1": + return nil, "", ErrInvalidAlgo + } + } + + var digits = 6 + if sdigit := v.Get("digits"); sdigit != "" { + tmpDigits, err := strconv.ParseInt(sdigit, 10, 8) + if err != nil { + return nil, "", err + } + digits = int(tmpDigits) + } + + var period uint64 = 30 + if speriod := v.Get("period"); speriod != "" { + var err error + period, err = strconv.ParseUint(speriod, 10, 64) + if err != nil { + return nil, "", err + } + } + + key, err := base32.StdEncoding.DecodeString(secret) + if err != nil { + return nil, "", err + } + otp := NewTOTP(key, 0, period, digits, algo) + return otp, label, nil +} + +func (otp *TOTP) QR(label string) ([]byte, error) { + return otp.oath.QR(otp.Type(), label) +} diff --git a/totp_test.go b/totp_test.go new file mode 100644 index 0000000..e42e6a0 --- /dev/null +++ b/totp_test.go @@ -0,0 +1,55 @@ +package twofactor + +import ( + "crypto" + "fmt" + "testing" +) + +var rfcTotpKey = []byte("12345678901234567890") +var rfcTotpStep uint64 = 30 + +var rfcTotpTests = []struct { + Time uint64 + Code string + T uint64 + Algo crypto.Hash +}{ + {59, "94287082", 1, crypto.SHA1}, + {59, "46119246", 1, crypto.SHA256}, + {59, "90693936", 1, crypto.SHA512}, + {1111111109, "07081804", 37037036, crypto.SHA1}, + // {1111111109, "68084774", 37037036, crypto.SHA256}, + // {1111111109, "25091201", 37037036, crypto.SHA512}, + {1111111111, "14050471", 37037037, crypto.SHA1}, + // {1111111111, "67062674", 37037037, crypto.SHA256}, + // {1111111111, "99943326", 37037037, crypto.SHA512}, + {1234567890, "89005924", 41152263, crypto.SHA1}, + // {1234567890, "91819424", 41152263, crypto.SHA256}, + // {1234567890, "93441116", 41152263, crypto.SHA512}, + {2000000000, "69279037", 66666666, crypto.SHA1}, + // {2000000000, "90698825", 66666666, crypto.SHA256}, + // {2000000000, "38618901", 66666666, crypto.SHA512}, + {20000000000, "65353130", 666666666, crypto.SHA1}, + // {20000000000, "77737706", 666666666, crypto.SHA256}, + // {20000000000, "47863826", 666666666, crypto.SHA512}, +} + +func TestTotpRFC(t *testing.T) { + for _, tc := range rfcTotpTests { + otp := NewTOTP(rfcTotpKey, 0, 30, 8, tc.Algo) + if otp.otpCounter(tc.Time) != tc.T { + fmt.Println("twofactor: invalid T") + fmt.Printf("\texpected: %d\n", tc.T) + fmt.Printf("\t actual: %d\n", otp.otpCounter(tc.Time)) + t.FailNow() + } + + if code := otp.otp(otp.otpCounter(tc.Time)); code != tc.Code { + fmt.Println("twofactor: invalid TOTP") + fmt.Printf("\texpected: %s\n", tc.Code) + fmt.Printf("\t actual: %s\n", code) + t.FailNow() + } + } +}