otp.go 4.3 KB

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