├── README.md ├── .gitignore ├── totp_test.go └── totp.go /README.md: -------------------------------------------------------------------------------- 1 | github.com/balasanjay/totp 2 | ==== 3 | 4 | A Go implementation of the Time-Based One Time Password (TOTP) protocol. Works 5 | with Google Authenticator. Docs can be read online at 6 | [godoc.org](http://godoc.org/github.com/balasanjay/totp). -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | .idea 24 | -------------------------------------------------------------------------------- /totp_test.go: -------------------------------------------------------------------------------- 1 | package totp 2 | 3 | import ( 4 | "crypto/sha1" 5 | "crypto/sha256" 6 | "crypto/sha512" 7 | "encoding/hex" 8 | "hash" 9 | "os" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestPrint(t *testing.T) { 15 | b, err := BarcodeImage("foo@bar.com", []byte("hello"), nil) 16 | if err != nil { 17 | t.Errorf("expecting no error, got %q", err) 18 | } 19 | 20 | if len(b) <= 0 { 21 | t.Errorf("expecting b to be non-empty") 22 | } 23 | 24 | return 25 | 26 | // This code is for manual testing of the library functionality 27 | 28 | // Authenticates to test authentication 29 | t.Logf("Authenticate=%v", Authenticate([]byte("hello"), "493478", nil)) 30 | 31 | // Creates a QR code 32 | f, err := os.Create("foo.png") 33 | if err != nil { 34 | t.Errorf("Could not create file: %v", err) 35 | return 36 | } 37 | 38 | _, err = f.Write(b) 39 | if err != nil { 40 | t.Errorf("Could not write barcode: %v", err) 41 | return 42 | } 43 | } 44 | 45 | func TestVarious(t *testing.T) { 46 | s20 := "3132333435363738393031323334353637383930" 47 | s32 := "3132333435363738393031323334353637383930" + 48 | "313233343536373839303132" 49 | s64 := "3132333435363738393031323334353637383930" + 50 | "3132333435363738393031323334353637383930" + 51 | "3132333435363738393031323334353637383930" + 52 | "31323334" 53 | 54 | var secrets [][]byte 55 | 56 | for _, v := range []string{s20, s32, s64} { 57 | sec, _ := hex.DecodeString(v) 58 | secrets = append(secrets, []byte(sec)) 59 | } 60 | 61 | tests := []struct { 62 | time int64 63 | totps []string 64 | }{ 65 | {time: 59, totps: []string{"94287082", "46119246", "90693936"}}, 66 | {time: 1111111109, totps: []string{"07081804", "68084774", "25091201"}}, 67 | {time: 1111111111, totps: []string{"14050471", "67062674", "99943326"}}, 68 | {time: 1234567890, totps: []string{"89005924", "91819424", "93441116"}}, 69 | {time: 2000000000, totps: []string{"69279037", "90698825", "38618901"}}, 70 | {time: 20000000000, totps: []string{"65353130", "77737706", "47863826"}}, 71 | } 72 | 73 | for _, c := range tests { 74 | for i, h := range []func() hash.Hash{sha1.New, sha256.New, sha512.New} { 75 | if i >= len(c.totps) { 76 | break 77 | } 78 | 79 | opt := NewOptions() 80 | opt.Time = func() time.Time { 81 | return time.Unix(c.time, 0) 82 | } 83 | opt.Tries = []int64{0} 84 | opt.TimeStep = 30 * time.Second 85 | opt.Digits = 8 86 | opt.Hash = h 87 | 88 | // Test the simple case 89 | auth := Authenticate(secrets[i], c.totps[i], opt) 90 | if !auth { 91 | t.Errorf("should have authenticated, but didn't. TOTP:%q Unix-Time:%v", c.totps[i], c.time) 92 | continue 93 | } 94 | 95 | // Test that the tries array works as intended 96 | newtime := c.time - int64(opt.TimeStep/time.Second) 97 | opt.Tries = []int64{0, 1} 98 | opt.Time = func() time.Time { 99 | return time.Unix(newtime, 0) 100 | } 101 | auth = Authenticate(secrets[i], c.totps[i], opt) 102 | if !auth { 103 | t.Errorf("should have authenticated, but didn't. TOTP:%q Unix-Time:%v", c.totps[i], newtime) 104 | continue 105 | } 106 | 107 | // Modify the TOTP, and make sure that it fails 108 | failtotp := []byte(c.totps[i]) 109 | failtotp[0] = ((failtotp[0]-'0')+1)%('9'-'0') + '0' 110 | auth = Authenticate(secrets[i], string(failtotp), opt) 111 | if auth { 112 | t.Errorf("should have failed to authenticate, but didnt. TOTP:%q Unix-Time:%v", failtotp, c.time) 113 | continue 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /totp.go: -------------------------------------------------------------------------------- 1 | // Package totp implements the Time-Based One-Time Password Algorithm, 2 | // specified in RFC 6238. It allows clients to implement Two-Factor 3 | // Authentication, and interoperates with Google Authenticator. 4 | package totp 5 | 6 | import ( 7 | "crypto/hmac" 8 | "crypto/sha1" 9 | "encoding/base32" 10 | "fmt" 11 | "hash" 12 | "net/url" 13 | "strconv" 14 | "time" 15 | 16 | "bytes" 17 | "image/png" 18 | 19 | qr "github.com/qpliu/qrencode-go/qrencode" 20 | "strings" 21 | ) 22 | 23 | // BarcodeImage creates a QR code for use with Google Authenticator (GA). 24 | // label is the string that GA uses in the UI. secretkey should be this user's 25 | // secret key. opt should be the configured Options for this TOTP. If a nil 26 | // options is passed, then DefaultOptions is used. 27 | func BarcodeImage(label string, secretkey []byte, opt *Options) ([]byte, error) { 28 | if opt == nil { 29 | opt = DefaultOptions 30 | } 31 | 32 | u := &url.URL{ 33 | Scheme: "otpauth", 34 | Host: "totp", 35 | Path: fmt.Sprintf("/%s", label), 36 | } 37 | 38 | params := url.Values{ 39 | "secret": {strings.TrimRight(base32.StdEncoding.EncodeToString(secretkey), "=")}, 40 | "digits": {strconv.Itoa(int(opt.Digits))}, 41 | "period": {strconv.Itoa(int(opt.TimeStep / time.Second))}, 42 | } 43 | 44 | u.RawQuery = params.Encode() 45 | 46 | c, err := qr.Encode(u.String(), qr.ECLevelM) 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | var buf bytes.Buffer 53 | 54 | err = png.Encode(&buf, c.Image(8)) 55 | 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return buf.Bytes(), nil 61 | } 62 | 63 | // Options contains the different configurable values for a given TOTP 64 | // invocation. 65 | type Options struct { 66 | Time func() time.Time 67 | Tries []int64 68 | TimeStep time.Duration 69 | Digits uint8 70 | Hash func() hash.Hash 71 | } 72 | 73 | // NewOptions constructs a pre-configured Options. The returned Options' uses 74 | // time.Now to get the current time, has a window size of 30 seconds, and 75 | // tries the currently active window, and the previous one. It expects 6 digits, 76 | // and uses sha1 for its hash algorithm. These settings were chosen to be 77 | // compatible with Google Authenticator. 78 | func NewOptions() *Options { 79 | return &Options{ 80 | Time: time.Now, 81 | Tries: []int64{0, -1}, 82 | TimeStep: 30 * time.Second, 83 | Digits: 6, 84 | Hash: sha1.New, 85 | } 86 | } 87 | 88 | var DefaultOptions = NewOptions() 89 | 90 | var digit_power = []int64{ 91 | 1, // 0 92 | 10, // 1 93 | 100, // 2 94 | 1000, // 3 95 | 10000, // 4 96 | 100000, // 5 97 | 1000000, // 6 98 | 10000000, // 7 99 | 100000000, // 8 100 | 1000000000, // 9 101 | } 102 | 103 | // Authenticate verifies the TOTP userCode taking the key from secretKey and 104 | // other options from o. If o is nil, then DefaultOptions is used instead. 105 | func Authenticate(secretKey []byte, userCode string, o *Options) bool { 106 | if o == nil { 107 | o = DefaultOptions 108 | } 109 | 110 | if int(o.Digits) != len(userCode) { 111 | return false 112 | } 113 | 114 | uc, err := strconv.ParseInt(userCode, 10, 64) 115 | if err != nil { 116 | return false 117 | } 118 | 119 | t := o.Time().Unix() / int64(o.TimeStep/time.Second) 120 | var tbuf [8]byte 121 | 122 | hm := hmac.New(o.Hash, secretKey) 123 | var hashbuf []byte 124 | 125 | for i := 0; i < len(o.Tries); i++ { 126 | b := t + o.Tries[i] 127 | 128 | tbuf[0] = byte(b >> 56) 129 | tbuf[1] = byte(b >> 48) 130 | tbuf[2] = byte(b >> 40) 131 | tbuf[3] = byte(b >> 32) 132 | tbuf[4] = byte(b >> 24) 133 | tbuf[5] = byte(b >> 16) 134 | tbuf[6] = byte(b >> 8) 135 | tbuf[7] = byte(b) 136 | 137 | hm.Reset() 138 | hm.Write(tbuf[:]) 139 | hashbuf = hm.Sum(hashbuf[:0]) 140 | 141 | offset := hashbuf[len(hashbuf)-1] & 0xf 142 | truncatedHash := hashbuf[offset:] 143 | 144 | code := int64(truncatedHash[0])<<24 | 145 | int64(truncatedHash[1])<<16 | 146 | int64(truncatedHash[2])<<8 | 147 | int64(truncatedHash[3]) 148 | 149 | code &= 0x7FFFFFFF 150 | code %= digit_power[len(userCode)] 151 | 152 | if code == uc { 153 | return true 154 | } 155 | } 156 | 157 | return false 158 | } 159 | --------------------------------------------------------------------------------