1/** 2 * Copyright 2014 Paul Querna 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18package totp 19 20import ( 21 "github.com/pquerna/otp" 22 "github.com/pquerna/otp/hotp" 23 "io" 24 25 "crypto/rand" 26 "encoding/base32" 27 "math" 28 "net/url" 29 "strconv" 30 "time" 31) 32 33// Validate a TOTP using the current time. 34// A shortcut for ValidateCustom, Validate uses a configuration 35// that is compatible with Google-Authenticator and most clients. 36func Validate(passcode string, secret string) bool { 37 rv, _ := ValidateCustom( 38 passcode, 39 secret, 40 time.Now().UTC(), 41 ValidateOpts{ 42 Period: 30, 43 Skew: 1, 44 Digits: otp.DigitsSix, 45 Algorithm: otp.AlgorithmSHA1, 46 }, 47 ) 48 return rv 49} 50 51// GenerateCode creates a TOTP token using the current time. 52// A shortcut for GenerateCodeCustom, GenerateCode uses a configuration 53// that is compatible with Google-Authenticator and most clients. 54func GenerateCode(secret string, t time.Time) (string, error) { 55 return GenerateCodeCustom(secret, t, ValidateOpts{ 56 Period: 30, 57 Skew: 1, 58 Digits: otp.DigitsSix, 59 Algorithm: otp.AlgorithmSHA1, 60 }) 61} 62 63// ValidateOpts provides options for ValidateCustom(). 64type ValidateOpts struct { 65 // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. 66 Period uint 67 // Periods before or after the current time to allow. Value of 1 allows up to Period 68 // of either side of the specified time. Defaults to 0 allowed skews. Values greater 69 // than 1 are likely sketchy. 70 Skew uint 71 // Digits as part of the input. Defaults to 6. 72 Digits otp.Digits 73 // Algorithm to use for HMAC. Defaults to SHA1. 74 Algorithm otp.Algorithm 75} 76 77// GenerateCodeCustom takes a timepoint and produces a passcode using a 78// secret and the provided opts. (Under the hood, this is making an adapted 79// call to hotp.GenerateCodeCustom) 80func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) { 81 if opts.Period == 0 { 82 opts.Period = 30 83 } 84 counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period))) 85 passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{ 86 Digits: opts.Digits, 87 Algorithm: opts.Algorithm, 88 }) 89 if err != nil { 90 return "", err 91 } 92 return passcode, nil 93} 94 95// ValidateCustom validates a TOTP given a user specified time and custom options. 96// Most users should use Validate() to provide an interpolatable TOTP experience. 97func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) { 98 if opts.Period == 0 { 99 opts.Period = 30 100 } 101 102 counters := []uint64{} 103 counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period))) 104 105 counters = append(counters, uint64(counter)) 106 for i := 1; i <= int(opts.Skew); i++ { 107 counters = append(counters, uint64(counter+int64(i))) 108 counters = append(counters, uint64(counter-int64(i))) 109 } 110 111 for _, counter := range counters { 112 rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{ 113 Digits: opts.Digits, 114 Algorithm: opts.Algorithm, 115 }) 116 117 if err != nil { 118 return false, err 119 } 120 121 if rv == true { 122 return true, nil 123 } 124 } 125 126 return false, nil 127} 128 129// GenerateOpts provides options for Generate(). The default values 130// are compatible with Google-Authenticator. 131type GenerateOpts struct { 132 // Name of the issuing Organization/Company. 133 Issuer string 134 // Name of the User's Account (eg, email address) 135 AccountName string 136 // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. 137 Period uint 138 // Size in size of the generated Secret. Defaults to 20 bytes. 139 SecretSize uint 140 // Secret to store. Defaults to a randomly generated secret of SecretSize. You should generally leave this empty. 141 Secret []byte 142 // Digits to request. Defaults to 6. 143 Digits otp.Digits 144 // Algorithm to use for HMAC. Defaults to SHA1. 145 Algorithm otp.Algorithm 146 // Reader to use for generating TOTP Key. 147 Rand io.Reader 148} 149 150var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) 151 152// Generate a new TOTP Key. 153func Generate(opts GenerateOpts) (*otp.Key, error) { 154 // url encode the Issuer/AccountName 155 if opts.Issuer == "" { 156 return nil, otp.ErrGenerateMissingIssuer 157 } 158 159 if opts.AccountName == "" { 160 return nil, otp.ErrGenerateMissingAccountName 161 } 162 163 if opts.Period == 0 { 164 opts.Period = 30 165 } 166 167 if opts.SecretSize == 0 { 168 opts.SecretSize = 20 169 } 170 171 if opts.Digits == 0 { 172 opts.Digits = otp.DigitsSix 173 } 174 175 if opts.Rand == nil { 176 opts.Rand = rand.Reader 177 } 178 179 // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example 180 181 v := url.Values{} 182 if len(opts.Secret) != 0 { 183 v.Set("secret", b32NoPadding.EncodeToString(opts.Secret)) 184 } else { 185 secret := make([]byte, opts.SecretSize) 186 _, err := opts.Rand.Read(secret) 187 if err != nil { 188 return nil, err 189 } 190 v.Set("secret", b32NoPadding.EncodeToString(secret)) 191 } 192 193 v.Set("issuer", opts.Issuer) 194 v.Set("period", strconv.FormatUint(uint64(opts.Period), 10)) 195 v.Set("algorithm", opts.Algorithm.String()) 196 v.Set("digits", opts.Digits.String()) 197 198 u := url.URL{ 199 Scheme: "otpauth", 200 Host: "totp", 201 Path: "/" + opts.Issuer + ":" + opts.AccountName, 202 RawQuery: v.Encode(), 203 } 204 205 return otp.NewKeyFromURL(u.String()) 206} 207