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