otp.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package mfa
  13. import (
  14. "crypto/hmac"
  15. "crypto/sha1" //nolint:gosec // not used for encryption purposes
  16. "crypto/sha256"
  17. "crypto/sha512"
  18. "encoding/base32"
  19. "encoding/binary"
  20. "errors"
  21. "fmt"
  22. "hash"
  23. "math"
  24. "strconv"
  25. "strings"
  26. "time"
  27. )
  28. const (
  29. // defaultLength for a token is 6 characters.
  30. defaultLength = 6
  31. // defaultTimePeriod for a token is 30 seconds.
  32. defaultTimePeriod = 30
  33. // defaultAlgorithm according to the RFC should be sha1.
  34. defaultAlgorithm = "sha1"
  35. )
  36. // options define configurable values for a TOTP token.
  37. type options struct {
  38. algorithm string
  39. when time.Time
  40. token string
  41. timePeriod int64
  42. length int
  43. }
  44. // GeneratorOptionsFunc provides a nice way of configuring the generator while allowing defaults.
  45. type GeneratorOptionsFunc func(*options)
  46. // WithToken can be used to set the token value.
  47. func WithToken(token string) GeneratorOptionsFunc {
  48. return func(o *options) {
  49. o.token = token
  50. }
  51. }
  52. // WithTimePeriod sets the time-period for the generated token. Default is 30s.
  53. func WithTimePeriod(timePeriod int64) GeneratorOptionsFunc {
  54. return func(o *options) {
  55. o.timePeriod = timePeriod
  56. }
  57. }
  58. // WithLength sets the length of the token. Defaults to 6 digits where the token can start with 0.
  59. func WithLength(length int) GeneratorOptionsFunc {
  60. return func(o *options) {
  61. o.length = length
  62. }
  63. }
  64. // WithAlgorithm configures the algorithm. Defaults to sha1.
  65. func WithAlgorithm(algorithm string) GeneratorOptionsFunc {
  66. return func(o *options) {
  67. o.algorithm = algorithm
  68. }
  69. }
  70. // WithWhen allows configuring the time when the token is generated from. Defaults to time.Now().
  71. func WithWhen(when time.Time) GeneratorOptionsFunc {
  72. return func(o *options) {
  73. o.when = when
  74. }
  75. }
  76. // generateCode generates an N digit TOTP code from the secret token.
  77. func generateCode(opts ...GeneratorOptionsFunc) (string, string, error) {
  78. defaults := &options{
  79. algorithm: defaultAlgorithm,
  80. length: defaultLength,
  81. timePeriod: defaultTimePeriod,
  82. when: time.Now(),
  83. }
  84. for _, opt := range opts {
  85. opt(defaults)
  86. }
  87. cleanUpToken(defaults)
  88. if defaults.length > math.MaxInt {
  89. return "", "", errors.New("length too big")
  90. }
  91. timer := uint64(math.Floor(float64(defaults.when.Unix()) / float64(defaults.timePeriod)))
  92. remainingTime := defaults.timePeriod - defaults.when.Unix()%defaults.timePeriod
  93. secretBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(defaults.token)
  94. if err != nil {
  95. return "", "", fmt.Errorf("failed to generate OTP code: %w", err)
  96. }
  97. buf := make([]byte, 8)
  98. shaFunc, err := getAlgorithmFunction(defaults.algorithm)
  99. if err != nil {
  100. return "", "", err
  101. }
  102. mac := hmac.New(shaFunc, secretBytes)
  103. binary.BigEndian.PutUint64(buf, timer)
  104. _, _ = mac.Write(buf)
  105. sum := mac.Sum(nil)
  106. // http://tools.ietf.org/html/rfc4226#section-5.4
  107. offset := sum[len(sum)-1] & 0xf
  108. value := ((int(sum[offset]) & 0x7f) << 24) |
  109. ((int(sum[offset+1] & 0xff)) << 16) |
  110. ((int(sum[offset+2] & 0xff)) << 8) |
  111. (int(sum[offset+3]) & 0xff)
  112. modulo := value % int(math.Pow10(defaults.length))
  113. format := fmt.Sprintf("%%0%dd", defaults.length)
  114. return fmt.Sprintf(format, modulo), strconv.Itoa(int(remainingTime)), nil
  115. }
  116. func cleanUpToken(defaults *options) {
  117. // Remove all spaces. Providers sometimes make it more readable by fragmentation.
  118. defaults.token = strings.ReplaceAll(defaults.token, " ", "")
  119. // The token is always uppercase.
  120. defaults.token = strings.ToUpper(defaults.token)
  121. }
  122. func getAlgorithmFunction(algo string) (func() hash.Hash, error) {
  123. switch algo {
  124. case "sha512":
  125. return sha512.New, nil
  126. case "sha384":
  127. return sha512.New384, nil
  128. case "sha512_256":
  129. return sha512.New512_256, nil
  130. case "sha256":
  131. return sha256.New, nil
  132. case "sha1":
  133. return sha1.New, nil
  134. default:
  135. return nil, fmt.Errorf("%s for hash function is invalid", algo)
  136. }
  137. }