totp.go 2.6 KB

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