├── .gitignore ├── .travis.yml ├── GODOC.md ├── LICENSE ├── Makefile ├── README.md ├── const.go ├── doc.go ├── hotp.go ├── hotp_test.go ├── misc.go ├── misc_test.go ├── otp └── main.go ├── totp.go └── totp_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | *.html 3 | *.swp 4 | otp/otp -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.8.x 4 | - 1.9 5 | install: 6 | - go get -v -t ./... 7 | - go get -v ./... 8 | -------------------------------------------------------------------------------- /GODOC.md: -------------------------------------------------------------------------------- 1 | # otp 2 | -- 3 | import "github.com/hgfischer/go-otp" 4 | 5 | Package otp implements one-time-password generators used in 2-factor 6 | authentication systems like RSA-tokens and Google Authenticator. Currently this 7 | supports both HOTP (RFC-4226) and TOTP (RFC-6238). 8 | 9 | All tests used in this package, uses reference values from both RFCs to ensure 10 | compatibility with another OTP implementations. 11 | 12 | ## Usage 13 | 14 | ```go 15 | const ( 16 | DefaultLength = 6 // Default length of the generated tokens 17 | DefaultPeriod = 30 // Default time period for TOTP tokens, in seconds 18 | DefaultRandomSecretLength = 100 // Default random secret length 19 | DefaultWindowBack = 1 // Default TOTP verification window back steps 20 | DefaultWindowForward = 1 // Default TOTP verification window forward steps 21 | ) 22 | ``` 23 | Default settings for all generators 24 | 25 | ```go 26 | const ( 27 | MaxLength = 10 // Maximum token length 28 | ) 29 | ``` 30 | Maximum values for all generators 31 | 32 | #### type HOTP 33 | 34 | ```go 35 | type HOTP struct { 36 | Secret string // The secret used to generate the token 37 | Length uint8 // The token size, with a maximum determined by MaxLength 38 | Counter uint64 // The counter used as moving factor 39 | IsBase32Secret bool // If true, the secret will be used as a Base32 encoded string 40 | } 41 | ``` 42 | 43 | HOTP is used to generate tokens based on RFC-4226. 44 | 45 | Example: 46 | 47 | hotp := &HOTP{Secret: "your-secret", Counter: 1000, Length: 8, IsBase32Secret: true} 48 | token := hotp.Get() 49 | 50 | HOTP assumes a set of default values for Secret, Length, Counter, and 51 | IsBase32Secret. If no Secret is informed, HOTP will generate a random one that 52 | you need to store with the Counter, for future token verifications. Check this 53 | package constants to see the current default values. 54 | 55 | #### func (*HOTP) Get 56 | 57 | ```go 58 | func (h *HOTP) Get() string 59 | ``` 60 | Get a token generated with the current HOTP settings 61 | 62 | #### type TOTP 63 | 64 | ```go 65 | type TOTP struct { 66 | Secret string // The secret used to generate a token 67 | Length uint8 // The token length 68 | Time time.Time // The time used to generate the token 69 | IsBase32Secret bool // 70 | Period uint8 // The step size to slice time, in seconds 71 | WindowBack uint8 // How many steps HOTP will go backwards to validate a token 72 | WindowForward uint8 // How many steps HOTP will go forward to validate a token 73 | } 74 | ``` 75 | 76 | TOTP is used to generate tokens based on RFC-6238. 77 | 78 | Example: 79 | 80 | totp := &TOTP{Secret: "your-secret", IsBase32Secret: true} 81 | token := totp.Get() 82 | 83 | TOTP assumes a set of default values for Secret, Length, Time, Period, 84 | WindowBack, WindowForward and IsBase32Secret 85 | 86 | If no Secret is informed, TOTP will generate a random one that you need to store 87 | with the Counter, for future token verifications. 88 | 89 | Check this package constants to see the current default values. 90 | 91 | #### func (*TOTP) Get 92 | 93 | ```go 94 | func (t *TOTP) Get() string 95 | ``` 96 | Get a time-based token 97 | 98 | #### func (*TOTP) Now 99 | 100 | ```go 101 | func (t *TOTP) Now() *TOTP 102 | ``` 103 | Now is a fluent interface to set the TOTP generator's time to the current 104 | date/time 105 | 106 | #### func (TOTP) Verify 107 | 108 | ```go 109 | func (t TOTP) Verify(token string) bool 110 | ``` 111 | Verify a token with the current settings, including the WindowBack and 112 | WindowForward 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Herbert G. Fischer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the organization nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL HERBERT G. FISCHER BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES := $(shell find . -type f -name '*.go') 2 | GOTOOLDIR := $(shell go env GOTOOLDIR) 3 | LINT := $(GOBIN)/golint 4 | GODOCDOWN := $(GOBIN)/godocdown 5 | VET := $(GOTOOLDIR)/vet 6 | COVER := $(GOTOOLDIR)/cover 7 | PKGS := $(shell go list ./...) 8 | PKG := $(shell go list) 9 | COVER_OUT := coverage.out 10 | COVER_HTML := coverage.html 11 | TMP_COVER := tmp_cover.out 12 | 13 | 14 | .PHONY: default 15 | default: test 16 | 17 | 18 | .PHONY: check_gopath 19 | check_gopath: 20 | ifndef GOPATH 21 | @echo "ERROR!! GOPATH must be declared. Check http://golang.org/doc/code.html#GOPATH" 22 | @exit 1 23 | endif 24 | ifeq ($(shell go list ./... | grep -q '^_'; echo $$?), 0) 25 | @echo "ERROR!! This directory should be at $(GOPATH)/src/$(REPO)" 26 | @exit 1 27 | endif 28 | @exit 0 29 | 30 | 31 | .PHONY: check_gobin 32 | check_gobin: 33 | ifndef GOBIN 34 | @echo "ERROR!! GOBIN must be declared. Check http://golang.org/doc/code.html#GOBIN" 35 | @exit 1 36 | endif 37 | @exit 0 38 | 39 | 40 | .PHONY: clean 41 | clean: check_gopath 42 | @echo "Removing temp files..." 43 | @rm -fv *.cover *.out *.html 44 | @go clean -v 45 | 46 | 47 | .PHONY: test 48 | test: $(SYMLINK) check_gopath 49 | @go get -t 50 | @for pkg in $(PKGS); do go test -v -race $$pkg || exit 1; done 51 | 52 | 53 | $(COVER): check_gopath check_gobin 54 | @go get code.google.com/p/go.tools/cmd/cover || exit 0 55 | 56 | .PHONY: cover 57 | cover: check_gopath $(COVER) 58 | @echo 'mode: set' > $(COVER_OUT) 59 | @touch $(TMP_COVER) 60 | @for pkg in $(PKGS); do \ 61 | go test -v -coverprofile=$(TMP_COVER) $$pkg || exit 1; \ 62 | grep -v 'mode: set' $(TMP_COVER) >> $(COVER_OUT); \ 63 | done 64 | @go tool cover -html=$(COVER_OUT) -o $(COVER_HTML) 65 | @(which gnome-open && gnome-open $(COVER_HTML)) || (which -s open && open $(COVER_HTML)) || (exit 0) 66 | @echo Generated HTML report in $(COVER_HTML)... 67 | 68 | 69 | $(LINT): check_gopath check_gobin 70 | @go get github.com/golang/lint/golint 71 | 72 | .PHONY: lint 73 | lint: $(LINT) 74 | @for src in $(SOURCES); do golint $$src || exit 1; done 75 | 76 | 77 | .PHONY: check_vet 78 | check_vet: 79 | @if [ ! -x $(VET) ]; then \ 80 | echo Missing Go vet tool! Please install with the following command...; \ 81 | echo sudo go get code.google.com/p/go.tools/cmd/vet; \ 82 | exit 1; \ 83 | fi 84 | 85 | .PHONY: vet 86 | vet: check_gopath check_vet 87 | @for src in $(SOURCES); do go tool vet $$src; done 88 | 89 | 90 | $(GODOCDOWN): check_gopath check_gobin 91 | @go get github.com/robertkrimen/godocdown/godocdown 92 | 93 | .PHONY: doc 94 | doc: $(GODOCDOWN) 95 | @godocdown $(PKG) > GODOC.md 96 | 97 | 98 | .PHONY: fmt 99 | fmt: 100 | @gofmt -s -w . 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OTP 2 | 3 | [![Build Status](https://travis-ci.org/hgfischer/go-otp.svg?branch=master)](https://travis-ci.org/hgfischer/go-otp) 4 | 5 | Package go-otp implements one-time-password generators used in 2-factor authentication systems like RSA-tokens and Google Authenticator. Currently this supports both HOTP (RFC-4226) and TOTP (RFC-6238). 6 | 7 | 8 | 9 | ## Install 10 | 11 | ``` 12 | $ go get github.com/hgfischer/go-otp 13 | ``` 14 | 15 | 16 | ## Usage 17 | 18 | Check API docs, with examples, at http://godoc.org/github.com/hgfischer/go-otp 19 | 20 | 21 | 22 | # License 23 | 24 | Copyright (c) 2014, Herbert G. Fischer 25 | All rights reserved. 26 | 27 | Redistribution and use in source and binary forms, with or without 28 | modification, are permitted provided that the following conditions are met: 29 | * Redistributions of source code must retain the above copyright 30 | notice, this list of conditions and the following disclaimer. 31 | * Redistributions in binary form must reproduce the above copyright 32 | notice, this list of conditions and the following disclaimer in the 33 | documentation and/or other materials provided with the distribution. 34 | * Neither the name of the organization nor the 35 | names of its contributors may be used to endorse or promote products 36 | derived from this software without specific prior written permission. 37 | 38 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 39 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 40 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 41 | DISCLAIMED. IN NO EVENT SHALL HERBERT G. FISCHER BE LIABLE FOR ANY 42 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 43 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 44 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 45 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 46 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 47 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 48 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | // Default settings for all generators 4 | const ( 5 | DefaultLength = 6 // Default length of the generated tokens 6 | DefaultPeriod = 30 // Default time period for TOTP tokens, in seconds 7 | DefaultRandomSecretLength = 100 // Default random secret length 8 | DefaultWindowBack = 1 // Default TOTP verification window back steps 9 | DefaultWindowForward = 1 // Default TOTP verification window forward steps 10 | ) 11 | 12 | // Maximum values for all generators 13 | const ( 14 | MaxLength = 10 // Maximum token length 15 | ) 16 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, Herbert G. Fischer 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // * Redistributions of source code must retain the above copyright 7 | // notice, this list of conditions and the following disclaimer. 8 | // * Redistributions in binary form must reproduce the above copyright 9 | // notice, this list of conditions and the following disclaimer in the 10 | // documentation and/or other materials provided with the distribution. 11 | // * Neither the name of the organization nor the 12 | // names of its contributors may be used to endorse or promote products 13 | // derived from this software without specific prior written permission. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | // DISCLAIMED. IN NO EVENT SHALL HERBERT G. FISCHER BE LIABLE FOR ANY 19 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | // Package otp implements one-time-password generators used in 2-factor authentication systems like RSA-tokens and Google Authenticator. Currently this supports both HOTP (RFC-4226) and TOTP (RFC-6238). 27 | // 28 | // All tests used in this package, uses reference values from both RFCs to ensure 29 | // compatibility with another OTP implementations. 30 | package otp 31 | -------------------------------------------------------------------------------- /hotp.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import ( 4 | "encoding/base32" 5 | "fmt" 6 | "math" 7 | ) 8 | 9 | // HOTP is used to generate tokens based on RFC-4226. 10 | // 11 | // Example: 12 | // 13 | // hotp := &HOTP{Secret: "your-secret", Counter: 1000, Length: 8, IsBase32Secret: true} 14 | // token := hotp.Get() 15 | // 16 | // HOTP assumes a set of default values for Secret, Length, Counter, and IsBase32Secret. 17 | // If no Secret is informed, HOTP will generate a random one that you need to store with 18 | // the Counter, for future token verifications. Check this package constants to see the 19 | // current default values. 20 | type HOTP struct { 21 | Secret string // The secret used to generate the token 22 | Length uint8 // The token size, with a maximum determined by MaxLength 23 | Counter uint64 // The counter used as moving factor 24 | IsBase32Secret bool // If true, the secret will be used as a Base32 encoded string 25 | } 26 | 27 | func (h *HOTP) setDefaults() { 28 | if len(h.Secret) == 0 { 29 | h.Secret = generateRandomSecret(DefaultRandomSecretLength, h.IsBase32Secret) 30 | } 31 | if h.Length == 0 { 32 | h.Length = DefaultLength 33 | } 34 | } 35 | 36 | func (h *HOTP) normalize() { 37 | if h.Length > MaxLength { 38 | h.Length = MaxLength 39 | } 40 | } 41 | 42 | // Get a token generated with the current HOTP settings 43 | func (h *HOTP) Get() string { 44 | h.setDefaults() 45 | h.normalize() 46 | text := counterToBytes(h.Counter) 47 | var hash []byte 48 | if h.IsBase32Secret { 49 | secretBytes, _ := base32.StdEncoding.DecodeString(h.Secret) 50 | hash = hmacSHA1(secretBytes, text) 51 | } else { 52 | hash = hmacSHA1([]byte(h.Secret), text) 53 | } 54 | binary := truncate(hash) 55 | otp := int64(binary) % int64(math.Pow10(int(h.Length))) 56 | hotp := fmt.Sprintf(fmt.Sprintf("%%0%dd", h.Length), otp) 57 | return hotp 58 | } 59 | -------------------------------------------------------------------------------- /hotp_test.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestHOTP(t *testing.T) { 10 | secret := `12345678901234567890` 11 | table := map[uint64]string{ 12 | 0: `755224`, 13 | 1: `287082`, 14 | 2: `359152`, 15 | 3: `969429`, 16 | 4: `338314`, 17 | 5: `254676`, 18 | 6: `287922`, 19 | 7: `162583`, 20 | 8: `399871`, 21 | 9: `520489`, 22 | } 23 | 24 | for cnt, expected := range table { 25 | hotp := &HOTP{Secret: secret, Counter: cnt, Length: 6} 26 | result := hotp.Get() 27 | assert.Equal(t, expected, result) 28 | } 29 | } 30 | 31 | func TestHOTPBase32(t *testing.T) { 32 | secret := `JBSWY3DPEHPK3PXP` 33 | table := map[uint64]string{ 34 | 0: `282760`, 35 | 1: `996554`, 36 | 2: `602287`, 37 | 3: `143627`, 38 | 4: `960129`, 39 | 5: `768897`, 40 | 6: `883951`, 41 | 7: `449891`, 42 | 8: `964230`, 43 | 9: `924769`, 44 | } 45 | 46 | for cnt, expected := range table { 47 | hotp := &HOTP{Secret: secret, Counter: cnt, Length: 6, IsBase32Secret: true} 48 | result := hotp.Get() 49 | assert.Equal(t, expected, result) 50 | } 51 | } 52 | 53 | func TestHOTPShouldBeCroppedToMaxLength(t *testing.T) { 54 | hotp := &HOTP{Length: 20} 55 | result := hotp.Get() 56 | assert.Equal(t, MaxLength, len(result)) 57 | } 58 | 59 | func TestHOTPShouldUseDefaultValues(t *testing.T) { 60 | hotp := &HOTP{} 61 | result := hotp.Get() 62 | assert.Equal(t, uint8(DefaultLength), hotp.Length) 63 | assert.NotEmpty(t, hotp.Secret) 64 | assert.Equal(t, hotp.Length, uint8(len(result))) 65 | } 66 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/rand" 6 | "crypto/sha1" 7 | "encoding/base32" 8 | ) 9 | 10 | func generateRandomSecret(size int, encodeToBase32 bool) string { 11 | alphanum := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 12 | var bytes = make([]byte, size) 13 | rand.Read(bytes) 14 | for i, b := range bytes { 15 | bytes[i] = alphanum[b%byte(len(alphanum))] 16 | } 17 | if encodeToBase32 { 18 | return base32.StdEncoding.EncodeToString(bytes) 19 | } 20 | return string(bytes) 21 | } 22 | 23 | func counterToBytes(counter uint64) (text []byte) { 24 | text = make([]byte, 8) 25 | for i := 7; i >= 0; i-- { 26 | text[i] = byte(counter & 0xff) 27 | counter = counter >> 8 28 | } 29 | return 30 | } 31 | 32 | func hmacSHA1(key, text []byte) []byte { 33 | H := hmac.New(sha1.New, key) 34 | H.Write([]byte(text)) 35 | return H.Sum(nil) 36 | } 37 | 38 | func truncate(hash []byte) int { 39 | offset := int(hash[len(hash)-1] & 0xf) 40 | return ((int(hash[offset]) & 0x7f) << 24) | 41 | ((int(hash[offset+1] & 0xff)) << 16) | 42 | ((int(hash[offset+2] & 0xff)) << 8) | 43 | (int(hash[offset+3]) & 0xff) 44 | } 45 | -------------------------------------------------------------------------------- /misc_test.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import ( 4 | "encoding/base32" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHMACSHA1(t *testing.T) { 12 | secret := `12345678901234567890` 13 | table := map[uint64]string{ 14 | 0: `cc93cf18508d94934c64b65d8ba7667fb7cde4b0`, 15 | 1: `75a48a19d4cbe100644e8ac1397eea747a2d33ab`, 16 | 2: `0bacb7fa082fef30782211938bc1c5e70416ff44`, 17 | 3: `66c28227d03a2d5529262ff016a1e6ef76557ece`, 18 | 4: `a904c900a64b35909874b33e61c5938a8e15ed1c`, 19 | 5: `a37e783d7b7233c083d4f62926c7a25f238d0316`, 20 | 6: `bc9cd28561042c83f219324d3c607256c03272ae`, 21 | 7: `a4fb960c0bc06e1eabb804e5b397cdc4b45596fa`, 22 | 8: `1b3c89f65e6c9e883012052823443f048b4332db`, 23 | 9: `1637409809a679dc698207310c8c7fc07290d9e5`, 24 | } 25 | 26 | for cnt, expected := range table { 27 | result := hmacSHA1([]byte(secret), counterToBytes(cnt)) 28 | assert.Equal(t, expected, fmt.Sprintf("%x", result)) 29 | } 30 | } 31 | 32 | func TestGenerateRandomSecret(t *testing.T) { 33 | // Brute force test to check if the returned string is 34 | // a valid Base32 string. 35 | for i := 0; i < 1000; i++ { 36 | secret := generateRandomSecret(20, true) 37 | _, err := base32.StdEncoding.DecodeString(secret) 38 | assert.Nil(t, err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /otp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base32" 5 | "flag" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/hgfischer/go-otp" 10 | ) 11 | 12 | var ( 13 | secret = flag.String("secret", "AAAABBBBCCCCDDDD", "Secret key") 14 | isBase32 = flag.Bool("base32", true, "If true, the secret is interpreted as a Base32 string") 15 | length = flag.Uint("length", otp.DefaultLength, "OTP length") 16 | period = flag.Uint("period", otp.DefaultPeriod, "Period in seconds") 17 | counter = flag.Uint64("counter", 0, "Counter") 18 | ) 19 | 20 | func main() { 21 | flag.Parse() 22 | 23 | key := *secret 24 | if !*isBase32 { 25 | key = base32.StdEncoding.EncodeToString([]byte(*secret)) 26 | } 27 | 28 | key = strings.ToUpper(key) 29 | if !isGoogleAuthenticatorCompatible(key) { 30 | fmt.Println("WARN: Google Authenticator requires 16 chars base32 secret, without padding") 31 | } 32 | 33 | fmt.Println("Secret Base32 Encoded Key: ", key) 34 | 35 | totp := &otp.TOTP{ 36 | Secret: key, 37 | Length: uint8(*length), 38 | Period: uint8(*period), 39 | IsBase32Secret: true, 40 | } 41 | fmt.Println("TOTP:", totp.Get()) 42 | 43 | hotp := &otp.HOTP{ 44 | Secret: key, 45 | Length: uint8(*length), 46 | Counter: *counter, 47 | IsBase32Secret: true, 48 | } 49 | 50 | fmt.Println("HOTP:", hotp.Get()) 51 | } 52 | 53 | func isGoogleAuthenticatorCompatible(base32Secret string) bool { 54 | cleaned := strings.Replace(base32Secret, "=", "", -1) 55 | cleaned = strings.Replace(cleaned, " ", "", -1) 56 | return len(cleaned) == 16 57 | } 58 | -------------------------------------------------------------------------------- /totp.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import "time" 4 | 5 | // TOTP is used to generate tokens based on RFC-6238. 6 | // 7 | // Example: 8 | // 9 | // totp := &TOTP{Secret: "your-secret", IsBase32Secret: true} 10 | // token := totp.Get() 11 | // 12 | // TOTP assumes a set of default values for Secret, Length, Time, Period, WindowBack, WindowForward and IsBase32Secret 13 | // 14 | // If no Secret is informed, TOTP will generate a random one that you need to store with the Counter, for future token 15 | // verifications. 16 | // 17 | // Check this package constants to see the current default values. 18 | type TOTP struct { 19 | Secret string // The secret used to generate a token 20 | Length uint8 // The token length 21 | Time time.Time // The time used to generate the token 22 | IsBase32Secret bool // 23 | Period uint8 // The step size to slice time, in seconds 24 | WindowBack uint8 // How many steps HOTP will go backwards to validate a token 25 | WindowForward uint8 // How many steps HOTP will go forward to validate a token 26 | } 27 | 28 | func (t *TOTP) setDefaults() { 29 | if len(t.Secret) == 0 { 30 | t.Secret = generateRandomSecret(DefaultRandomSecretLength, t.IsBase32Secret) 31 | } 32 | if t.Length == 0 { 33 | t.Length = DefaultLength 34 | } 35 | if t.Time.IsZero() { 36 | t.Time = time.Now() 37 | } 38 | if t.Period == 0 { 39 | t.Period = DefaultPeriod 40 | } 41 | if t.WindowBack == 0 { 42 | t.WindowBack = DefaultWindowBack 43 | } 44 | if t.WindowForward == 0 { 45 | t.WindowForward = DefaultWindowForward 46 | } 47 | } 48 | 49 | func (t *TOTP) normalize() { 50 | if t.Length > MaxLength { 51 | t.Length = MaxLength 52 | } 53 | } 54 | 55 | // Get a time-based token 56 | func (t *TOTP) Get() string { 57 | t.setDefaults() 58 | t.normalize() 59 | ts := uint64(t.Time.Unix() / int64(t.Period)) 60 | hotp := &HOTP{Secret: t.Secret, Counter: ts, Length: t.Length, IsBase32Secret: t.IsBase32Secret} 61 | return hotp.Get() 62 | } 63 | 64 | // Now is a fluent interface to set the TOTP generator's time to the current date/time 65 | func (t *TOTP) Now() *TOTP { 66 | t.Time = time.Now() 67 | return t 68 | } 69 | 70 | // Verify a token with the current settings, including the WindowBack and WindowForward 71 | func (t TOTP) Verify(token string) bool { 72 | t.setDefaults() 73 | t.normalize() 74 | givenTime := t.Time 75 | for i := int(t.WindowBack) * -1; i <= int(t.WindowForward); i++ { 76 | t.Time = givenTime.Add(time.Second * time.Duration(int(t.Period)*i)) 77 | if t.Get() == token { 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | -------------------------------------------------------------------------------- /totp_test.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTOTP(t *testing.T) { 11 | secret := `12345678901234567890` 12 | table := map[time.Time]string{ 13 | time.Date(1970, 1, 1, 0, 0, 59, 0, time.UTC): `94287082`, 14 | time.Date(2005, 3, 18, 1, 58, 29, 0, time.UTC): `07081804`, 15 | time.Date(2005, 3, 18, 1, 58, 31, 0, time.UTC): `14050471`, 16 | time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC): `89005924`, 17 | time.Date(2033, 5, 18, 3, 33, 20, 0, time.UTC): `69279037`, 18 | time.Date(2603, 10, 11, 11, 33, 20, 0, time.UTC): `65353130`, 19 | } 20 | 21 | for tm, expected := range table { 22 | totp := &TOTP{Secret: secret, Length: 8, Time: tm, Period: 30} 23 | result := totp.Get() 24 | assert.Equal(t, expected, result, tm.String()) 25 | assert.True(t, totp.Verify(result)) 26 | } 27 | } 28 | 29 | func TestTOTPBase32(t *testing.T) { 30 | secret := `JBSWY3DPEHPK3PXP` 31 | table := map[time.Time]string{ 32 | time.Date(1970, 1, 1, 0, 0, 59, 0, time.UTC): `41996554`, 33 | time.Date(2005, 3, 18, 1, 58, 29, 0, time.UTC): `33071271`, 34 | time.Date(2005, 3, 18, 1, 58, 31, 0, time.UTC): `28358462`, 35 | time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC): `94742275`, 36 | time.Date(2033, 5, 18, 3, 33, 20, 0, time.UTC): `28890699`, 37 | time.Date(2603, 10, 11, 11, 33, 20, 0, time.UTC): `94752434`, 38 | } 39 | 40 | for tm, expected := range table { 41 | totp := &TOTP{Secret: secret, Length: 8, Time: tm, Period: 30, IsBase32Secret: true} 42 | result := totp.Get() 43 | assert.Equal(t, expected, result, tm.String()) 44 | assert.True(t, totp.Verify(result)) 45 | } 46 | } 47 | 48 | func TestTOTPShouldBeCroppedToMaxLength(t *testing.T) { 49 | totp := &TOTP{Length: 20} 50 | result := totp.Get() 51 | assert.Equal(t, MaxLength, len(result)) 52 | } 53 | 54 | func TestTOTPShouldUseDefaultValues(t *testing.T) { 55 | totp := &TOTP{} 56 | result := totp.Get() 57 | assert.NotEmpty(t, totp.Secret) 58 | assert.Equal(t, uint8(DefaultLength), totp.Length) 59 | assert.False(t, totp.Time.IsZero()) 60 | assert.Equal(t, totp.Length, uint8(len(result))) 61 | } 62 | 63 | func TestTOTPShouldUseCurrentTimeWithFluentInterface(t *testing.T) { 64 | past := time.Date(1979, 3, 26, 19, 30, 0, 0, time.Local) 65 | now := time.Now().Format(time.Kitchen) 66 | totp := &TOTP{Time: past} 67 | totp.Now().Get() 68 | assert.Equal(t, now, totp.Time.Format(time.Kitchen)) 69 | } 70 | 71 | func TestTOTPVerifyShouldFail(t *testing.T) { 72 | past := time.Now().Add(time.Second * DefaultPeriod * -2) 73 | totp := &TOTP{Time: past} 74 | token := totp.Get() 75 | assert.False(t, totp.Now().Verify(token)) 76 | } 77 | 78 | func TestTOTPVerifyShouldSucceedWithinWindowForward(t *testing.T) { 79 | future := time.Now().Add(time.Second * DefaultPeriod * 3) 80 | totp := &TOTP{Time: future, WindowForward: 3} 81 | token := totp.Get() 82 | assert.True(t, totp.Now().Verify(token)) 83 | } 84 | 85 | func TestTOTPVerifyShouldSucceedWithinWindowBack(t *testing.T) { 86 | past := time.Now().Add(time.Second * DefaultPeriod * -3) 87 | totp := &TOTP{Time: past, WindowBack: 3} 88 | token := totp.Get() 89 | assert.True(t, totp.Now().Verify(token)) 90 | } 91 | 92 | func TestTOTPVerifyShouldFailOutOfWindowForward(t *testing.T) { 93 | future := time.Now().Add(time.Second * DefaultPeriod * 4) 94 | totp := &TOTP{Time: future, WindowForward: 3} 95 | token := totp.Get() 96 | assert.False(t, totp.Now().Verify(token)) 97 | } 98 | 99 | func TestTOTPVerifyShouldFailOutOfWindowBack(t *testing.T) { 100 | past := time.Now().Add(time.Second * DefaultPeriod * -4) 101 | totp := &TOTP{Time: past, WindowBack: 3} 102 | token := totp.Get() 103 | assert.False(t, totp.Now().Verify(token)) 104 | } 105 | --------------------------------------------------------------------------------