You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
5.1 KiB

  1. /**
  2. * Copyright 2014 Paul Querna
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package totp
  18. import (
  19. "github.com/pquerna/otp"
  20. "github.com/pquerna/otp/hotp"
  21. "crypto/rand"
  22. "encoding/base32"
  23. "math"
  24. "net/url"
  25. "strconv"
  26. "time"
  27. )
  28. // Validate a TOTP using the current time.
  29. // A shortcut for ValidateCustom, Validate uses a configuration
  30. // that is compatible with Google-Authenticator and most clients.
  31. func Validate(passcode string, secret string) bool {
  32. rv, _ := ValidateCustom(
  33. passcode,
  34. secret,
  35. time.Now().UTC(),
  36. ValidateOpts{
  37. Period: 30,
  38. Skew: 1,
  39. Digits: otp.DigitsSix,
  40. Algorithm: otp.AlgorithmSHA1,
  41. },
  42. )
  43. return rv
  44. }
  45. // GenerateCode creates a TOTP token using the current time.
  46. // A shortcut for GenerateCodeCustom, GenerateCode uses a configuration
  47. // that is compatible with Google-Authenticator and most clients.
  48. func GenerateCode(secret string, t time.Time) (string, error) {
  49. return GenerateCodeCustom(secret, t, ValidateOpts{
  50. Period: 30,
  51. Skew: 1,
  52. Digits: otp.DigitsSix,
  53. Algorithm: otp.AlgorithmSHA1,
  54. })
  55. }
  56. // ValidateOpts provides options for ValidateCustom().
  57. type ValidateOpts struct {
  58. // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
  59. Period uint
  60. // Periods before or after the current time to allow. Value of 1 allows up to Period
  61. // of either side of the specified time. Defaults to 0 allowed skews. Values greater
  62. // than 1 are likely sketchy.
  63. Skew uint
  64. // Digits as part of the input. Defaults to 6.
  65. Digits otp.Digits
  66. // Algorithm to use for HMAC. Defaults to SHA1.
  67. Algorithm otp.Algorithm
  68. }
  69. // GenerateCodeCustom takes a timepoint and produces a passcode using a
  70. // secret and the provided opts. (Under the hood, this is making an adapted
  71. // call to hotp.GenerateCodeCustom)
  72. func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) {
  73. if opts.Period == 0 {
  74. opts.Period = 30
  75. }
  76. counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
  77. passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{
  78. Digits: opts.Digits,
  79. Algorithm: opts.Algorithm,
  80. })
  81. if err != nil {
  82. return "", err
  83. }
  84. return passcode, nil
  85. }
  86. // ValidateCustom validates a TOTP given a user specified time and custom options.
  87. // Most users should use Validate() to provide an interpolatable TOTP experience.
  88. func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) {
  89. if opts.Period == 0 {
  90. opts.Period = 30
  91. }
  92. counters := []uint64{}
  93. counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
  94. counters = append(counters, uint64(counter))
  95. for i := 1; i <= int(opts.Skew); i++ {
  96. counters = append(counters, uint64(counter+int64(i)))
  97. counters = append(counters, uint64(counter-int64(i)))
  98. }
  99. for _, counter := range counters {
  100. rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{
  101. Digits: opts.Digits,
  102. Algorithm: opts.Algorithm,
  103. })
  104. if err != nil {
  105. return false, err
  106. }
  107. if rv == true {
  108. return true, nil
  109. }
  110. }
  111. return false, nil
  112. }
  113. // GenerateOpts provides options for Generate(). The default values
  114. // are compatible with Google-Authenticator.
  115. type GenerateOpts struct {
  116. // Name of the issuing Organization/Company.
  117. Issuer string
  118. // Name of the User's Account (eg, email address)
  119. AccountName string
  120. // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
  121. Period uint
  122. // Size in size of the generated Secret. Defaults to 10 bytes.
  123. SecretSize uint
  124. // Digits to request. Defaults to 6.
  125. Digits otp.Digits
  126. // Algorithm to use for HMAC. Defaults to SHA1.
  127. Algorithm otp.Algorithm
  128. }
  129. // Generate a new TOTP Key.
  130. func Generate(opts GenerateOpts) (*otp.Key, error) {
  131. // url encode the Issuer/AccountName
  132. if opts.Issuer == "" {
  133. return nil, otp.ErrGenerateMissingIssuer
  134. }
  135. if opts.AccountName == "" {
  136. return nil, otp.ErrGenerateMissingAccountName
  137. }
  138. if opts.Period == 0 {
  139. opts.Period = 30
  140. }
  141. if opts.SecretSize == 0 {
  142. opts.SecretSize = 10
  143. }
  144. if opts.Digits == 0 {
  145. opts.Digits = otp.DigitsSix
  146. }
  147. // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
  148. v := url.Values{}
  149. secret := make([]byte, opts.SecretSize)
  150. _, err := rand.Read(secret)
  151. if err != nil {
  152. return nil, err
  153. }
  154. v.Set("secret", base32.StdEncoding.EncodeToString(secret))
  155. v.Set("issuer", opts.Issuer)
  156. v.Set("period", strconv.FormatUint(uint64(opts.Period), 10))
  157. v.Set("algorithm", opts.Algorithm.String())
  158. v.Set("digits", opts.Digits.String())
  159. u := url.URL{
  160. Scheme: "otpauth",
  161. Host: "totp",
  162. Path: "/" + opts.Issuer + ":" + opts.AccountName,
  163. RawQuery: v.Encode(),
  164. }
  165. return otp.NewKeyFromURL(u.String())
  166. }