1package mfa 2 3import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "image/png" 8 "sync" 9 "time" 10 11 "github.com/pquerna/otp" 12 "github.com/pquerna/otp/totp" 13) 14 15// TOTPHMacAlgo is the enumerable for the possible HMAC algorithms for Time-based one time passwords 16type TOTPHMacAlgo = string 17 18// supported TOTP HMAC algorithms 19const ( 20 TOTPAlgoSHA1 TOTPHMacAlgo = "sha1" 21 TOTPAlgoSHA256 TOTPHMacAlgo = "sha256" 22 TOTPAlgoSHA512 TOTPHMacAlgo = "sha512" 23) 24 25var ( 26 cleanupTicker *time.Ticker 27 cleanupDone chan bool 28 usedPasscodes sync.Map 29 errPasscodeUsed = errors.New("this passcode was already used") 30) 31 32// TOTPConfig defines the configuration for a Time-based one time password 33type TOTPConfig struct { 34 Name string `json:"name" mapstructure:"name"` 35 Issuer string `json:"issuer" mapstructure:"issuer"` 36 Algo TOTPHMacAlgo `json:"algo" mapstructure:"algo"` 37 algo otp.Algorithm 38} 39 40func (c *TOTPConfig) validate() error { 41 if c.Name == "" { 42 return errors.New("totp: name is mandatory") 43 } 44 if c.Issuer == "" { 45 return errors.New("totp: issuer is mandatory") 46 } 47 switch c.Algo { 48 case TOTPAlgoSHA1: 49 c.algo = otp.AlgorithmSHA1 50 case TOTPAlgoSHA256: 51 c.algo = otp.AlgorithmSHA256 52 case TOTPAlgoSHA512: 53 c.algo = otp.AlgorithmSHA512 54 default: 55 return fmt.Errorf("unsupported totp algo %#v", c.Algo) 56 } 57 return nil 58} 59 60// validatePasscode validates a TOTP passcode 61func (c *TOTPConfig) validatePasscode(passcode, secret string) (bool, error) { 62 key := fmt.Sprintf("%v_%v", secret, passcode) 63 if _, ok := usedPasscodes.Load(key); ok { 64 return false, errPasscodeUsed 65 } 66 match, err := totp.ValidateCustom(passcode, secret, time.Now().UTC(), totp.ValidateOpts{ 67 Period: 30, 68 Skew: 1, 69 Digits: otp.DigitsSix, 70 Algorithm: c.algo, 71 }) 72 if match && err == nil { 73 usedPasscodes.Store(key, time.Now().Add(1*time.Minute).UTC()) 74 } 75 return match, err 76} 77 78// generate generates a new TOTP secret and QR code for the given username 79func (c *TOTPConfig) generate(username string, qrCodeWidth, qrCodeHeight int) (string, string, []byte, error) { 80 key, err := totp.Generate(totp.GenerateOpts{ 81 Issuer: c.Issuer, 82 AccountName: username, 83 Digits: otp.DigitsSix, 84 Algorithm: c.algo, 85 }) 86 if err != nil { 87 return "", "", nil, err 88 } 89 var buf bytes.Buffer 90 img, err := key.Image(qrCodeWidth, qrCodeHeight) 91 if err != nil { 92 return "", "", nil, err 93 } 94 err = png.Encode(&buf, img) 95 return key.Issuer(), key.Secret(), buf.Bytes(), err 96} 97 98func cleanupUsedPasscodes() { 99 usedPasscodes.Range(func(key, value interface{}) bool { 100 exp, ok := value.(time.Time) 101 if !ok || exp.Before(time.Now().UTC()) { 102 usedPasscodes.Delete(key) 103 } 104 return true 105 }) 106} 107