twofactor-1.0.1/0000755000175000017500000000000013266670672013326 5ustar michaelmichaeltwofactor-1.0.1/totp_test.go0000644000175000017500000000321313265726371015676 0ustar michaelmichaelpackage 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() } } } twofactor-1.0.1/util.go0000644000175000017500000000044013265726371014625 0ustar michaelmichaelpackage twofactor import ( "strings" ) // Pad calculates the number of '='s to add to our encoded string // to make base32.StdEncoding.DecodeString happy func Pad(s string) string { if !strings.HasSuffix(s, "=") && len(s)%8 != 0 { for len(s)%8 != 0 { s += "=" } } return s } twofactor-1.0.1/hotp_test.go0000644000175000017500000000313113265726371015661 0ustar michaelmichaelpackage twofactor import ( "fmt" "testing" ) var testKey = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} func newZeroHOTP() *HOTP { return NewHOTP(testKey, 0, 6) } var rfcHotpKey = []byte("12345678901234567890") var rfcHotpExpected = []string{ "755224", "287082", "359152", "969429", "338314", "254676", "287922", "162583", "399871", "520489", } // This test runs through the test cases presented in the RFC, and // ensures that this implementation is in compliance. func TestHotpRFC(t *testing.T) { otp := NewHOTP(rfcHotpKey, 0, 6) for i := 0; i < len(rfcHotpExpected); i++ { if otp.Counter() != uint64(i) { fmt.Printf("twofactor: invalid counter (should be %d, is %d", i, otp.Counter()) t.FailNow() } code := otp.OTP() if code == "" { fmt.Printf("twofactor: failed to produce an OTP\n") t.FailNow() } else if code != rfcHotpExpected[i] { fmt.Printf("twofactor: invalid OTP\n") fmt.Printf("\tExpected: %s\n", rfcHotpExpected[i]) fmt.Printf("\t Actual: %s\n", code) fmt.Printf("\t Counter: %d\n", otp.counter) t.FailNow() } } } // This test uses a different key than the test cases in the RFC, // but runs through the same test cases to ensure that they fail as // expected. func TestHotpBadRFC(t *testing.T) { otp := NewHOTP(testKey, 0, 6) for i := 0; i < len(rfcHotpExpected); i++ { code := otp.OTP() if code == "" { fmt.Printf("twofactor: failed to produce an OTP\n") t.FailNow() } else if code == rfcHotpExpected[i] { fmt.Printf("twofactor: should not have received a valid OTP\n") t.FailNow() } } } twofactor-1.0.1/otp.go0000644000175000017500000000315413265726371014457 0ustar michaelmichaelpackage twofactor import ( "crypto/rand" "errors" "fmt" "hash" "net/url" ) type Type uint const ( OATH_HOTP = iota OATH_TOTP ) // PRNG is an io.Reader that provides a cryptographically secure // random byte stream. var PRNG = rand.Reader var ( ErrInvalidURL = errors.New("twofactor: invalid URL") ErrInvalidAlgo = errors.New("twofactor: invalid algorithm") ) // Type OTP represents a one-time password token -- whether a // software taken (as in the case of Google Authenticator) or a // hardware token (as in the case of a YubiKey). type OTP interface { // Returns the current counter value; the meaning of the // returned value is algorithm-specific. Counter() uint64 // Set the counter to a specific value. SetCounter(uint64) // the secret key contained in the OTP Key() []byte // generate a new OTP OTP() string // the output size of the OTP Size() int // the hash function used by the OTP Hash() func() hash.Hash // Returns the type of this OTP. Type() Type } func otpString(otp OTP) string { var typeName string switch otp.Type() { case OATH_HOTP: typeName = "OATH-HOTP" case OATH_TOTP: typeName = "OATH-TOTP" default: typeName = "UNKNOWN" } return fmt.Sprintf("%s, %d", typeName, otp.Size()) } // FromURL constructs a new OTP token from a URL string. func FromURL(URL string) (OTP, string, error) { u, err := url.Parse(URL) if err != nil { return nil, "", err } if u.Scheme != "otpauth" { return nil, "", ErrInvalidURL } switch { case u.Host == "totp": return totpFromURL(u) case u.Host == "hotp": return hotpFromURL(u) default: return nil, "", ErrInvalidURL } } twofactor-1.0.1/doc.go0000644000175000017500000000026313265726371014420 0ustar michaelmichael// twofactor implements two-factor authentication. // // Currently supported are RFC 4226 HOTP one-time passwords and // RFC 6238 TOTP SHA-1 one-time passwords. package twofactor twofactor-1.0.1/.travis.yml0000644000175000017500000000010413265726371015427 0ustar michaelmichaelsudo: false language: go go: - 1.8 - 1.9 - "1.10" - master twofactor-1.0.1/hotp.go0000644000175000017500000000427613265726371014635 0ustar michaelmichaelpackage twofactor import ( "crypto" "crypto/sha1" "encoding/base32" "io" "net/url" "strconv" "strings" ) // HOTP represents an RFC-4226 Hash-based One Time Password instance. type HOTP struct { *OATH } // Type returns OATH_HOTP. func (otp *HOTP) Type() Type { return OATH_HOTP } // NewHOTP takes the key, the initial counter value, and the number // of digits (typically 6 or 8) and returns a new HOTP instance. func NewHOTP(key []byte, counter uint64, digits int) *HOTP { return &HOTP{ OATH: &OATH{ key: key, counter: counter, size: digits, hash: sha1.New, algo: crypto.SHA1, }, } } // OTP returns the next OTP and increments the counter. func (otp *HOTP) OTP() string { code := otp.OATH.OTP(otp.counter) otp.counter++ return code } // URL returns an HOTP URL (i.e. for putting in a QR code). func (otp *HOTP) URL(label string) string { return otp.OATH.URL(otp.Type(), label) } // SetProvider sets up the provider component of the OTP URL. func (otp *HOTP) SetProvider(provider string) { otp.provider = provider } // GenerateGoogleHOTP generates a new HOTP instance as used by // Google Authenticator. func GenerateGoogleHOTP() *HOTP { key := make([]byte, sha1.Size) if _, err := io.ReadFull(PRNG, key); err != nil { return nil } return NewHOTP(key, 0, 6) } func hotpFromURL(u *url.URL) (*HOTP, string, error) { label := u.Path[1:] v := u.Query() secret := strings.ToUpper(v.Get("secret")) if secret == "" { return nil, "", ErrInvalidURL } 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 counter uint64 = 0 if scounter := v.Get("counter"); scounter != "" { var err error counter, err = strconv.ParseUint(scounter, 10, 64) if err != nil { return nil, "", err } } key, err := base32.StdEncoding.DecodeString(Pad(secret)) if err != nil { // assume secret isn't base32 encoded key = []byte(secret) } otp := NewHOTP(key, counter, digits) return otp, label, nil } // QR generates a new QR code for the HOTP. func (otp *HOTP) QR(label string) ([]byte, error) { return otp.OATH.QR(otp.Type(), label) } twofactor-1.0.1/LICENSE0000644000175000017500000000205413265726371014331 0ustar michaelmichaelCopyright (c) 2017 Kyle Isom Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. twofactor-1.0.1/totp.go0000644000175000017500000000724613265726371014651 0ustar michaelmichaelpackage twofactor import ( "crypto" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/base32" "hash" "io" "net/url" "strconv" "strings" "time" ) // TOTP represents an RFC 6238 Time-based One-Time Password instance. type TOTP struct { *OATH step uint64 } // Type returns OATH_TOTP. func (otp *TOTP) Type() Type { return OATH_TOTP } func (otp *TOTP) otp(counter uint64) string { return otp.OATH.OTP(counter) } // OTP returns the OTP for the current timestep. func (otp *TOTP) OTP() string { return otp.otp(otp.OTPCounter()) } // URL returns a TOTP URL (i.e. for putting in a QR code). func (otp *TOTP) URL(label string) string { return otp.OATH.URL(otp.Type(), label) } // SetProvider sets up the provider component of the OTP URL. func (otp *TOTP) SetProvider(provider string) { otp.provider = provider } func (otp *TOTP) otpCounter(t uint64) uint64 { return (t - otp.counter) / otp.step } // OTPCounter returns the current time value for the OTP. func (otp *TOTP) OTPCounter() uint64 { return otp.otpCounter(uint64(time.Now().Unix())) } // NewOTP 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 { 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) } // 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) { label := u.Path[1:] v := u.Query() secret := strings.ToUpper(v.Get("secret")) if secret == "" { return nil, "", ErrInvalidURL } var algo = crypto.SHA1 if algorithm := v.Get("algorithm"); algorithm != "" { if strings.EqualFold(algorithm, "SHA256") { algo = crypto.SHA256 } else if strings.EqualFold(algorithm, "SHA512") { algo = crypto.SHA512 } else if !strings.EqualFold(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(Pad(secret)) if err != nil { // assume secret isn't base32 encoded key = []byte(secret) } otp := NewTOTP(key, 0, period, digits, algo) return otp, label, nil } // QR generates a new TOTP QR code. func (otp *TOTP) QR(label string) ([]byte, error) { return otp.OATH.QR(otp.Type(), label) } twofactor-1.0.1/Gopkg.lock0000644000175000017500000000070313265726371015244 0ustar michaelmichael# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. [[projects]] branch = "master" name = "rsc.io/qr" packages = [ ".", "coding", "gf256" ] revision = "48b2ede4844e13f1a2b7ce4d2529c9af7e359fc5" [solve-meta] analyzer-name = "dep" analyzer-version = 1 inputs-digest = "b705d306da5a78e76b7ff289744770eef56328a4f0d1c615c1a233d056283651" solver-name = "gps-cdcl" solver-version = 1 twofactor-1.0.1/otp_test.go0000644000175000017500000000715313265726371015521 0ustar michaelmichaelpackage twofactor import "fmt" import "io" import "testing" func TestHOTPString(t *testing.T) { hotp := NewHOTP(nil, 0, 6) hotpString := otpString(hotp) if hotpString != "OATH-HOTP, 6" { fmt.Println("twofactor: invalid OTP string") t.FailNow() } } // This test generates a new OTP, outputs the URL for that OTP, // and attempts to parse that URL. It verifies that the two OTPs // are the same, and that they produce the same output. func TestURL(t *testing.T) { var ident = "testuser@foo" otp := NewHOTP(testKey, 0, 6) url := otp.URL("testuser@foo") otp2, id, err := FromURL(url) if err != nil { fmt.Printf("hotp: failed to parse HOTP URL\n") t.FailNow() } else if id != ident { fmt.Printf("hotp: bad label\n") fmt.Printf("\texpected: %s\n", ident) fmt.Printf("\t actual: %s\n", id) t.FailNow() } else if otp2.Counter() != otp.Counter() { fmt.Printf("hotp: OTP counters aren't synced\n") fmt.Printf("\toriginal: %d\n", otp.Counter()) fmt.Printf("\t second: %d\n", otp2.Counter()) t.FailNow() } code1 := otp.OTP() code2 := otp2.OTP() if code1 != code2 { fmt.Printf("hotp: mismatched OTPs\n") fmt.Printf("\texpected: %s\n", code1) fmt.Printf("\t actual: %s\n", code2) } // There's not much we can do test the QR code, except to // ensure it doesn't fail. _, err = otp.QR(ident) if err != nil { fmt.Printf("hotp: failed to generate QR code PNG (%v)\n", err) t.FailNow() } // This should fail because the maximum size of an alphanumeric // QR code with the lowest-level of error correction should // max out at 4296 bytes. 8k may be a bit overkill... but it // gets the job done. The value is read from the PRNG to // increase the likelihood that the returned data is // uncompressible. var tooBigIdent = make([]byte, 8192) _, err = io.ReadFull(PRNG, tooBigIdent) if err != nil { fmt.Printf("hotp: failed to read identity (%v)\n", err) t.FailNow() } else if _, err = otp.QR(string(tooBigIdent)); err == nil { fmt.Println("hotp: QR code should fail to encode oversized URL") t.FailNow() } } // This test makes sure we can generate codes for padded and non-padded // entries func TestPaddedURL(t *testing.T) { var urlList = []string{ "otpauth://hotp/?secret=ME", "otpauth://hotp/?secret=MEFR", "otpauth://hotp/?secret=MFRGG", "otpauth://hotp/?secret=MFRGGZA", "otpauth://hotp/?secret=a6mryljlbufszudtjdt42nh5by=======", "otpauth://hotp/?secret=a6mryljlbufszudtjdt42nh5by", "otpauth://hotp/?secret=a6mryljlbufszudtjdt42nh5by%3D%3D%3D%3D%3D%3D%3D", } var codeList = []string{ "413198", "770938", "670717", "402378", "069864", "069864", "069864", } for i := range urlList { if o, id, err := FromURL(urlList[i]); err != nil { fmt.Println("hotp: URL should have parsed successfully") fmt.Printf("\turl was: %s\n", urlList[i]) t.FailNow() fmt.Printf("\t%s, %s\n", o.OTP(), id) } else { code2 := o.OTP() if code2 != codeList[i] { fmt.Printf("hotp: mismatched OTPs\n") fmt.Printf("\texpected: %s\n", codeList[i]) fmt.Printf("\t actual: %s\n", code2) t.FailNow() } } } } // This test attempts a variety of invalid urls against the parser // to ensure they fail. func TestBadURL(t *testing.T) { var urlList = []string{ "http://google.com", "", "-", "foo", "otpauth:/foo/bar/baz", "://", "otpauth://hotp/?digits=", "otpauth://hotp/?secret=MFRGGZDF&digits=ABCD", "otpauth://hotp/?secret=MFRGGZDF&counter=ABCD", } for i := range urlList { if _, _, err := FromURL(urlList[i]); err == nil { fmt.Println("hotp: URL should not have parsed successfully") fmt.Printf("\turl was: %s\n", urlList[i]) t.FailNow() } } } twofactor-1.0.1/Gopkg.toml0000644000175000017500000000131013265726371015262 0ustar michaelmichael# Gopkg.toml example # # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html # for detailed Gopkg.toml documentation. # # required = ["github.com/user/thing/cmd/thing"] # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] # # [[constraint]] # name = "github.com/user/project" # version = "1.0.0" # # [[constraint]] # name = "github.com/user/project2" # branch = "dev" # source = "github.com/myfork/project2" # # [[override]] # name = "github.com/x/y" # version = "2.4.0" # # [prune] # non-go = false # go-tests = true # unused-packages = true [[constraint]] branch = "master" name = "rsc.io/qr" [prune] go-tests = true unused-packages = true twofactor-1.0.1/util_test.go0000644000175000017500000000231713265726371015671 0ustar michaelmichaelpackage twofactor import ( "encoding/base32" "fmt" "math/rand" "strings" "testing" ) const letters = "1234567890!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randString() string { b := make([]byte, rand.Intn(len(letters))) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return base32.StdEncoding.EncodeToString(b) } func TestPadding(t *testing.T) { for i := 0; i < 300; i++ { b := randString() origEncoding := string(b) modEncoding := strings.Replace(string(b), "=", "", -1) str, err := base32.StdEncoding.DecodeString(origEncoding) if err != nil { fmt.Println("Can't decode: ", string(b)) t.FailNow() } paddedEncoding := Pad(modEncoding) if origEncoding != paddedEncoding { fmt.Println("Padding failed:") fmt.Printf("Expected: '%s'", origEncoding) fmt.Printf("Got: '%s'", paddedEncoding) t.FailNow() } else { mstr, err := base32.StdEncoding.DecodeString(paddedEncoding) if err != nil { fmt.Println("Can't decode: ", paddedEncoding) t.FailNow() } if string(mstr) != string(str) { fmt.Println("Re-padding failed:") fmt.Printf("Expected: '%s'", str) fmt.Printf("Got: '%s'", mstr) t.FailNow() } } } } twofactor-1.0.1/README.md0000644000175000017500000000260113265726371014601 0ustar michaelmichael## `twofactor` [![GoDoc](https://godoc.org/github.com/gokyle/twofactor?status.svg)](https://godoc.org/github.com/gokyle/twofactor) [![Build Status](https://travis-ci.org/gokyle/twofactor.svg?branch=master)](https://travis-ci.org/gokyle/twofactor) ### Author `twofactor` was written by Kyle Isom . ### License ``` Copyright (c) 2017 Kyle Isom Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` twofactor-1.0.1/oath_test.go0000644000175000017500000000117413265726371015647 0ustar michaelmichaelpackage 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() } } twofactor-1.0.1/oath.go0000644000175000017500000000606713265726371014616 0ustar michaelmichaelpackage twofactor import ( "crypto" "crypto/hmac" "encoding/base32" "encoding/binary" "fmt" "hash" "net/url" "rsc.io/qr" ) const defaultSize = 6 // OATH provides a baseline structure for the two OATH algorithms. type OATH struct { key []byte counter uint64 size int hash func() hash.Hash algo crypto.Hash provider string } // Size returns the output size (in characters) of the password. func (o OATH) Size() int { return o.size } // Counter returns the OATH token's counter. func (o OATH) Counter() uint64 { return o.counter } // SetCounter updates the OATH token's counter to a new value. func (o OATH) SetCounter(counter uint64) { o.counter = counter } // Key returns the token's secret key. func (o OATH) Key() []byte { return o.key[:] } // Hash returns the token's hash function. func (o OATH) Hash() func() hash.Hash { return o.hash } // URL constructs a URL appropriate for the token (i.e. for use in a // QR code). func (o OATH) URL(t Type, label string) string { secret := base32.StdEncoding.EncodeToString(o.key) u := url.URL{} v := url.Values{} u.Scheme = "otpauth" switch t { case OATH_HOTP: u.Host = "hotp" case OATH_TOTP: u.Host = "totp" } u.Path = label v.Add("secret", secret) if o.Counter() != 0 && t == OATH_HOTP { v.Add("counter", fmt.Sprintf("%d", o.Counter())) } if o.Size() != defaultSize { v.Add("digits", fmt.Sprintf("%d", o.Size())) } switch { case o.algo == crypto.SHA256: v.Add("algorithm", "SHA256") case o.algo == crypto.SHA512: v.Add("algorithm", "SHA512") } if o.provider != "" { v.Add("provider", o.provider) } u.RawQuery = v.Encode() return u.String() } var digits = []int64{ 0: 1, 1: 10, 2: 100, 3: 1000, 4: 10000, 5: 100000, 6: 1000000, 7: 10000000, 8: 100000000, 9: 1000000000, 10: 10000000000, } // The top-level type should provide a counter; for example, HOTP // will provide the counter directly while TOTP will provide the // time-stepped counter. func (o OATH) OTP(counter uint64) string { var ctr [8]byte binary.BigEndian.PutUint64(ctr[:], counter) var mod int64 = 1 if len(digits) > o.size { for i := 1; i <= o.size; i++ { mod *= 10 } } else { mod = digits[o.size] } h := hmac.New(o.hash, o.key) h.Write(ctr[:]) dt := truncate(h.Sum(nil)) % mod fmtStr := fmt.Sprintf("%%0%dd", o.size) return fmt.Sprintf(fmtStr, dt) } // truncate contains the DT function from the RFC; this is used to // deterministically select a sequence of 4 bytes from the HMAC // counter hash. func truncate(in []byte) int64 { offset := int(in[len(in)-1] & 0xF) p := in[offset : offset+4] var binCode int32 binCode = int32((p[0] & 0x7f)) << 24 binCode += int32((p[1] & 0xff)) << 16 binCode += int32((p[2] & 0xff)) << 8 binCode += int32((p[3] & 0xff)) return int64(binCode) & 0x7FFFFFFF } // QR generates a byte slice containing the a QR code encoded as a // PNG with level Q error correction. func (o OATH) QR(t Type, label string) ([]byte, error) { u := o.URL(t, label) code, err := qr.Encode(u, qr.Q) if err != nil { return nil, err } return code.PNG(), nil }