totp.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. // Copyright (C) 2019 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package mfa
  15. import (
  16. "bytes"
  17. "errors"
  18. "fmt"
  19. "image/png"
  20. "sync"
  21. "time"
  22. "github.com/pquerna/otp"
  23. "github.com/pquerna/otp/totp"
  24. )
  25. // TOTPHMacAlgo is the enumerable for the possible HMAC algorithms for Time-based one time passwords
  26. type TOTPHMacAlgo = string
  27. // supported TOTP HMAC algorithms
  28. const (
  29. TOTPAlgoSHA1 TOTPHMacAlgo = "sha1"
  30. TOTPAlgoSHA256 TOTPHMacAlgo = "sha256"
  31. TOTPAlgoSHA512 TOTPHMacAlgo = "sha512"
  32. )
  33. var (
  34. cleanupTicker *time.Ticker
  35. cleanupDone chan bool
  36. usedPasscodes sync.Map
  37. errPasscodeUsed = errors.New("this passcode was already used")
  38. )
  39. // TOTPConfig defines the configuration for a Time-based one time password
  40. type TOTPConfig struct {
  41. Name string `json:"name" mapstructure:"name"`
  42. Issuer string `json:"issuer" mapstructure:"issuer"`
  43. Algo TOTPHMacAlgo `json:"algo" mapstructure:"algo"`
  44. algo otp.Algorithm
  45. }
  46. func (c *TOTPConfig) validate() error {
  47. if c.Name == "" {
  48. return errors.New("totp: name is mandatory")
  49. }
  50. if c.Issuer == "" {
  51. return errors.New("totp: issuer is mandatory")
  52. }
  53. switch c.Algo {
  54. case TOTPAlgoSHA1:
  55. c.algo = otp.AlgorithmSHA1
  56. case TOTPAlgoSHA256:
  57. c.algo = otp.AlgorithmSHA256
  58. case TOTPAlgoSHA512:
  59. c.algo = otp.AlgorithmSHA512
  60. default:
  61. return fmt.Errorf("unsupported totp algo %q", c.Algo)
  62. }
  63. return nil
  64. }
  65. // validatePasscode validates a TOTP passcode
  66. func (c *TOTPConfig) validatePasscode(passcode, secret string) (bool, error) {
  67. key := fmt.Sprintf("%v_%v", secret, passcode)
  68. if _, ok := usedPasscodes.Load(key); ok {
  69. return false, errPasscodeUsed
  70. }
  71. match, err := totp.ValidateCustom(passcode, secret, time.Now().UTC(), totp.ValidateOpts{
  72. Period: 30,
  73. Skew: 1,
  74. Digits: otp.DigitsSix,
  75. Algorithm: c.algo,
  76. })
  77. if match && err == nil {
  78. usedPasscodes.Store(key, time.Now().Add(1*time.Minute).UTC())
  79. }
  80. return match, err
  81. }
  82. // generate generates a new TOTP secret and QR code for the given username
  83. func (c *TOTPConfig) generate(username string, qrCodeWidth, qrCodeHeight int) (*otp.Key, []byte, error) {
  84. key, err := totp.Generate(totp.GenerateOpts{
  85. Issuer: c.Issuer,
  86. AccountName: username,
  87. Digits: otp.DigitsSix,
  88. Algorithm: c.algo,
  89. })
  90. if err != nil {
  91. return nil, nil, err
  92. }
  93. var buf bytes.Buffer
  94. img, err := key.Image(qrCodeWidth, qrCodeHeight)
  95. if err != nil {
  96. return nil, nil, err
  97. }
  98. err = png.Encode(&buf, img)
  99. return key, buf.Bytes(), err
  100. }
  101. func cleanupUsedPasscodes() {
  102. usedPasscodes.Range(func(key, value any) bool {
  103. exp, ok := value.(time.Time)
  104. if !ok || exp.Before(time.Now().UTC()) {
  105. usedPasscodes.Delete(key)
  106. }
  107. return true
  108. })
  109. }