git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otp/otp.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 otp 19 20 import ( 21 "crypto/md5" 22 "crypto/sha1" 23 "crypto/sha512" 24 "errors" 25 "fmt" 26 "hash" 27 "image" 28 "net/url" 29 "strconv" 30 "strings" 31 32 "crypto/sha256" 33 34 "git.sr.ht/~pingoo/stdx/barcode" 35 "git.sr.ht/~pingoo/stdx/barcode/qr" 36 ) 37 38 // Error when attempting to convert the secret from base32 to raw bytes. 39 var ErrValidateSecretInvalidBase32 = errors.New("Decoding of secret as base32 failed.") 40 41 // The user provided passcode length was not expected. 42 var ErrValidateInputInvalidLength = errors.New("Input length unexpected") 43 44 // When generating a Key, the Issuer must be set. 45 var ErrGenerateMissingIssuer = errors.New("Issuer must be set") 46 47 // When generating a Key, the Account Name must be set. 48 var ErrGenerateMissingAccountName = errors.New("AccountName must be set") 49 50 // Key represents an TOTP or HTOP key. 51 type Key struct { 52 orig string 53 url *url.URL 54 } 55 56 // NewKeyFromURL creates a new Key from an TOTP or HOTP url. 57 // 58 // The URL format is documented here: 59 // 60 // https://github.com/google/google-authenticator/wiki/Key-Uri-Format 61 func NewKeyFromURL(orig string) (*Key, error) { 62 s := strings.TrimSpace(orig) 63 64 u, err := url.Parse(s) 65 if err != nil { 66 return nil, err 67 } 68 69 return &Key{ 70 orig: s, 71 url: u, 72 }, nil 73 } 74 75 func (k *Key) String() string { 76 return k.orig 77 } 78 79 // QrCode returns an QR-Code image of the specified width and height, 80 // suitable for use by many clients like Google-Authenricator 81 // to enroll a user's TOTP/HOTP key. 82 func (k *Key) QrCode(width int, height int) (image.Image, error) { 83 b, err := qr.Encode(k.orig, qr.M, qr.Auto) 84 if err != nil { 85 return nil, err 86 } 87 88 b, err = barcode.Scale(b, width, height) 89 90 if err != nil { 91 return nil, err 92 } 93 94 return b, nil 95 } 96 97 // Type returns "hotp" or "totp". 98 func (k *Key) Type() string { 99 return k.url.Host 100 } 101 102 // Issuer returns the name of the issuing organization. 103 func (k *Key) Issuer() string { 104 q := k.url.Query() 105 106 issuer := q.Get("issuer") 107 108 if issuer != "" { 109 return issuer 110 } 111 112 p := strings.TrimPrefix(k.url.Path, "/") 113 i := strings.Index(p, ":") 114 115 if i == -1 { 116 return "" 117 } 118 119 return p[:i] 120 } 121 122 // AccountName returns the name of the user's account. 123 func (k *Key) AccountName() string { 124 p := strings.TrimPrefix(k.url.Path, "/") 125 i := strings.Index(p, ":") 126 127 if i == -1 { 128 return p 129 } 130 131 return p[i+1:] 132 } 133 134 // Secret returns the opaque secret for this Key. 135 func (k *Key) Secret() string { 136 q := k.url.Query() 137 138 return q.Get("secret") 139 } 140 141 // Period returns a tiny int representing the rotation time in seconds. 142 func (k *Key) Period() uint64 { 143 q := k.url.Query() 144 145 if u, err := strconv.ParseUint(q.Get("period"), 10, 64); err == nil { 146 return u 147 } 148 149 // If no period is defined 30 seconds is the default per (rfc6238) 150 return 30 151 } 152 153 // Digits returns a tiny int representing the number of OTP digits. 154 func (k *Key) Digits() Digits { 155 q := k.url.Query() 156 157 if u, err := strconv.ParseUint(q.Get("digits"), 10, 64); err == nil { 158 switch u { 159 case 8: 160 return DigitsEight 161 default: 162 return DigitsSix 163 } 164 } 165 166 // Six is the most common value. 167 return DigitsSix 168 } 169 170 // Algorithm returns the algorithm used or the default (SHA1). 171 func (k *Key) Algorithm() Algorithm { 172 q := k.url.Query() 173 174 a := strings.ToLower(q.Get("algorithm")) 175 switch a { 176 case "md5": 177 return AlgorithmMD5 178 case "sha256": 179 return AlgorithmSHA256 180 case "sha512": 181 return AlgorithmSHA512 182 default: 183 return AlgorithmSHA1 184 } 185 } 186 187 // URL returns the OTP URL as a string 188 func (k *Key) URL() string { 189 return k.url.String() 190 } 191 192 // Algorithm represents the hashing function to use in the HMAC 193 // operation needed for OTPs. 194 type Algorithm int 195 196 const ( 197 // AlgorithmSHA1 should be used for compatibility with Google Authenticator. 198 // 199 // See https://git.sr.ht/~pingoo/stdx/otp/issues/55 for additional details. 200 AlgorithmSHA1 Algorithm = iota 201 AlgorithmSHA256 202 AlgorithmSHA512 203 AlgorithmMD5 204 ) 205 206 func (a Algorithm) String() string { 207 switch a { 208 case AlgorithmSHA1: 209 return "SHA1" 210 case AlgorithmSHA256: 211 return "SHA256" 212 case AlgorithmSHA512: 213 return "SHA512" 214 case AlgorithmMD5: 215 return "MD5" 216 } 217 panic("unreached") 218 } 219 220 func (algo Algorithm) Hash() hash.Hash { 221 switch algo { 222 case AlgorithmSHA1: 223 return sha1.New() 224 case AlgorithmSHA256: 225 return sha256.New() 226 case AlgorithmSHA512: 227 return sha512.New() 228 case AlgorithmMD5: 229 return md5.New() 230 } 231 panic("unreached") 232 } 233 234 // Digits represents the number of digits present in the 235 // user's OTP passcode. Six and Eight are the most common values. 236 type Digits int 237 238 const ( 239 DigitsSix Digits = 6 240 DigitsEight Digits = 8 241 ) 242 243 // Format converts an integer into the zero-filled size for this Digits. 244 func (d Digits) Format(in int32) string { 245 f := fmt.Sprintf("%%0%dd", d) 246 return fmt.Sprintf(f, in) 247 } 248 249 // Length returns the number of characters for this Digits. 250 func (d Digits) Length() int { 251 return int(d) 252 } 253 254 func (d Digits) String() string { 255 return fmt.Sprintf("%d", d) 256 }