git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otp/hotp/hotp.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 hotp 19 20 import ( 21 "io" 22 23 "git.sr.ht/~pingoo/stdx/otp" 24 25 "crypto/hmac" 26 "crypto/rand" 27 "crypto/subtle" 28 "encoding/base32" 29 "encoding/binary" 30 "fmt" 31 "math" 32 "net/url" 33 "strings" 34 ) 35 36 const debug = false 37 38 // Validate a HOTP passcode given a counter and secret. 39 // This is a shortcut for ValidateCustom, with parameters that 40 // are compataible with Google-Authenticator. 41 func Validate(passcode string, counter uint64, secret string) bool { 42 rv, _ := ValidateCustom( 43 passcode, 44 counter, 45 secret, 46 ValidateOpts{ 47 Digits: otp.DigitsSix, 48 Algorithm: otp.AlgorithmSHA1, 49 }, 50 ) 51 return rv 52 } 53 54 // ValidateOpts provides options for ValidateCustom(). 55 type ValidateOpts struct { 56 // Digits as part of the input. Defaults to 6. 57 Digits otp.Digits 58 // Algorithm to use for HMAC. Defaults to SHA1. 59 Algorithm otp.Algorithm 60 } 61 62 // GenerateCode creates a HOTP passcode given a counter and secret. 63 // This is a shortcut for GenerateCodeCustom, with parameters that 64 // are compataible with Google-Authenticator. 65 func GenerateCode(secret string, counter uint64) (string, error) { 66 return GenerateCodeCustom(secret, counter, ValidateOpts{ 67 Digits: otp.DigitsSix, 68 Algorithm: otp.AlgorithmSHA1, 69 }) 70 } 71 72 // GenerateCodeCustom uses a counter and secret value and options struct to 73 // create a passcode. 74 func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) { 75 // As noted in issue #10 and #17 this adds support for TOTP secrets that are 76 // missing their padding. 77 secret = strings.TrimSpace(secret) 78 if n := len(secret) % 8; n != 0 { 79 secret = secret + strings.Repeat("=", 8-n) 80 } 81 82 // As noted in issue #24 Google has started producing base32 in lower case, 83 // but the StdEncoding (and the RFC), expect a dictionary of only upper case letters. 84 secret = strings.ToUpper(secret) 85 86 secretBytes, err := base32.StdEncoding.DecodeString(secret) 87 if err != nil { 88 return "", otp.ErrValidateSecretInvalidBase32 89 } 90 91 buf := make([]byte, 8) 92 mac := hmac.New(opts.Algorithm.Hash, secretBytes) 93 binary.BigEndian.PutUint64(buf, counter) 94 if debug { 95 fmt.Printf("counter=%v\n", counter) 96 fmt.Printf("buf=%v\n", buf) 97 } 98 99 mac.Write(buf) 100 sum := mac.Sum(nil) 101 102 // "Dynamic truncation" in RFC 4226 103 // http://tools.ietf.org/html/rfc4226#section-5.4 104 offset := sum[len(sum)-1] & 0xf 105 value := int64(((int(sum[offset]) & 0x7f) << 24) | 106 ((int(sum[offset+1] & 0xff)) << 16) | 107 ((int(sum[offset+2] & 0xff)) << 8) | 108 (int(sum[offset+3]) & 0xff)) 109 110 l := opts.Digits.Length() 111 mod := int32(value % int64(math.Pow10(l))) 112 113 if debug { 114 fmt.Printf("offset=%v\n", offset) 115 fmt.Printf("value=%v\n", value) 116 fmt.Printf("mod'ed=%v\n", mod) 117 } 118 119 return opts.Digits.Format(mod), nil 120 } 121 122 // ValidateCustom validates an HOTP with customizable options. Most users should 123 // use Validate(). 124 func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) { 125 passcode = strings.TrimSpace(passcode) 126 127 if len(passcode) != opts.Digits.Length() { 128 return false, otp.ErrValidateInputInvalidLength 129 } 130 131 otpstr, err := GenerateCodeCustom(secret, counter, opts) 132 if err != nil { 133 return false, err 134 } 135 136 if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 { 137 return true, nil 138 } 139 140 return false, nil 141 } 142 143 // GenerateOpts provides options for .Generate() 144 type GenerateOpts struct { 145 // Name of the issuing Organization/Company. 146 Issuer string 147 // Name of the User's Account (eg, email address) 148 AccountName string 149 // Size in size of the generated Secret. Defaults to 10 bytes. 150 SecretSize uint 151 // Secret to store. Defaults to a randomly generated secret of SecretSize. You should generally leave this empty. 152 Secret []byte 153 // Digits to request. Defaults to 6. 154 Digits otp.Digits 155 // Algorithm to use for HMAC. Defaults to SHA1. 156 Algorithm otp.Algorithm 157 // Reader to use for generating HOTP Key. 158 Rand io.Reader 159 } 160 161 var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) 162 163 // Generate creates a new HOTP Key. 164 func Generate(opts GenerateOpts) (*otp.Key, error) { 165 // url encode the Issuer/AccountName 166 if opts.Issuer == "" { 167 return nil, otp.ErrGenerateMissingIssuer 168 } 169 170 if opts.AccountName == "" { 171 return nil, otp.ErrGenerateMissingAccountName 172 } 173 174 if opts.SecretSize == 0 { 175 opts.SecretSize = 10 176 } 177 178 if opts.Digits == 0 { 179 opts.Digits = otp.DigitsSix 180 } 181 182 if opts.Rand == nil { 183 opts.Rand = rand.Reader 184 } 185 186 // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example 187 188 v := url.Values{} 189 if len(opts.Secret) != 0 { 190 v.Set("secret", b32NoPadding.EncodeToString(opts.Secret)) 191 } else { 192 secret := make([]byte, opts.SecretSize) 193 _, err := opts.Rand.Read(secret) 194 if err != nil { 195 return nil, err 196 } 197 v.Set("secret", b32NoPadding.EncodeToString(secret)) 198 } 199 200 v.Set("issuer", opts.Issuer) 201 v.Set("algorithm", opts.Algorithm.String()) 202 v.Set("digits", opts.Digits.String()) 203 204 u := url.URL{ 205 Scheme: "otpauth", 206 Host: "hotp", 207 Path: "/" + opts.Issuer + ":" + opts.AccountName, 208 RawQuery: v.Encode(), 209 } 210 211 return otp.NewKeyFromURL(u.String()) 212 }