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.

181 lines
4.7 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 hotp
  18. import (
  19. "github.com/pquerna/otp"
  20. "crypto/hmac"
  21. "crypto/rand"
  22. "crypto/subtle"
  23. "encoding/base32"
  24. "encoding/binary"
  25. "fmt"
  26. "math"
  27. "net/url"
  28. "strings"
  29. )
  30. const debug = false
  31. // Validate a HOTP passcode given a counter and secret.
  32. // This is a shortcut for ValidateCustom, with parameters that
  33. // are compataible with Google-Authenticator.
  34. func Validate(passcode string, counter uint64, secret string) bool {
  35. rv, _ := ValidateCustom(
  36. passcode,
  37. counter,
  38. secret,
  39. ValidateOpts{
  40. Digits: otp.DigitsSix,
  41. Algorithm: otp.AlgorithmSHA1,
  42. },
  43. )
  44. return rv
  45. }
  46. // ValidateOpts provides options for ValidateCustom().
  47. type ValidateOpts struct {
  48. // Digits as part of the input. Defaults to 6.
  49. Digits otp.Digits
  50. // Algorithm to use for HMAC. Defaults to SHA1.
  51. Algorithm otp.Algorithm
  52. }
  53. // GenerateCode creates a HOTP passcode given a counter and secret.
  54. // This is a shortcut for GenerateCodeCustom, with parameters that
  55. // are compataible with Google-Authenticator.
  56. func GenerateCode(secret string, counter uint64) (string, error) {
  57. return GenerateCodeCustom(secret, counter, ValidateOpts{
  58. Digits: otp.DigitsSix,
  59. Algorithm: otp.AlgorithmSHA1,
  60. })
  61. }
  62. // GenerateCodeCustom uses a counter and secret value and options struct to
  63. // create a passcode.
  64. func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) {
  65. secretBytes, err := base32.StdEncoding.DecodeString(secret)
  66. if err != nil {
  67. return "", otp.ErrValidateSecretInvalidBase32
  68. }
  69. buf := make([]byte, 8)
  70. mac := hmac.New(opts.Algorithm.Hash, secretBytes)
  71. binary.BigEndian.PutUint64(buf, counter)
  72. if debug {
  73. fmt.Printf("counter=%v\n", counter)
  74. fmt.Printf("buf=%v\n", buf)
  75. }
  76. mac.Write(buf)
  77. sum := mac.Sum(nil)
  78. // "Dynamic truncation" in RFC 4226
  79. // http://tools.ietf.org/html/rfc4226#section-5.4
  80. offset := sum[len(sum)-1] & 0xf
  81. value := int64(((int(sum[offset]) & 0x7f) << 24) |
  82. ((int(sum[offset+1] & 0xff)) << 16) |
  83. ((int(sum[offset+2] & 0xff)) << 8) |
  84. (int(sum[offset+3]) & 0xff))
  85. l := opts.Digits.Length()
  86. mod := int32(value % int64(math.Pow10(l)))
  87. if debug {
  88. fmt.Printf("offset=%v\n", offset)
  89. fmt.Printf("value=%v\n", value)
  90. fmt.Printf("mod'ed=%v\n", mod)
  91. }
  92. return opts.Digits.Format(mod), nil
  93. }
  94. // ValidateCustom validates an HOTP with customizable options. Most users should
  95. // use Validate().
  96. func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
  97. passcode = strings.TrimSpace(passcode)
  98. if len(passcode) != opts.Digits.Length() {
  99. return false, otp.ErrValidateInputInvalidLength
  100. }
  101. otpstr, err := GenerateCodeCustom(secret, counter, opts)
  102. if err != nil {
  103. return false, err
  104. }
  105. if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 {
  106. return true, nil
  107. }
  108. return false, nil
  109. }
  110. // GenerateOpts provides options for .Generate()
  111. type GenerateOpts struct {
  112. // Name of the issuing Organization/Company.
  113. Issuer string
  114. // Name of the User's Account (eg, email address)
  115. AccountName string
  116. // Size in size of the generated Secret. Defaults to 10 bytes.
  117. SecretSize uint
  118. // Digits to request. Defaults to 6.
  119. Digits otp.Digits
  120. // Algorithm to use for HMAC. Defaults to SHA1.
  121. Algorithm otp.Algorithm
  122. }
  123. // Generate creates a new HOTP Key.
  124. func Generate(opts GenerateOpts) (*otp.Key, error) {
  125. // url encode the Issuer/AccountName
  126. if opts.Issuer == "" {
  127. return nil, otp.ErrGenerateMissingIssuer
  128. }
  129. if opts.AccountName == "" {
  130. return nil, otp.ErrGenerateMissingAccountName
  131. }
  132. if opts.SecretSize == 0 {
  133. opts.SecretSize = 10
  134. }
  135. // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
  136. v := url.Values{}
  137. secret := make([]byte, opts.SecretSize)
  138. _, err := rand.Read(secret)
  139. if err != nil {
  140. return nil, err
  141. }
  142. v.Set("secret", base32.StdEncoding.EncodeToString(secret))
  143. v.Set("issuer", opts.Issuer)
  144. v.Set("algorithm", opts.Algorithm.String())
  145. v.Set("digits", opts.Digits.String())
  146. u := url.URL{
  147. Scheme: "otpauth",
  148. Host: "hotp",
  149. Path: "/" + opts.Issuer + ":" + opts.AccountName,
  150. RawQuery: v.Encode(),
  151. }
  152. return otp.NewKeyFromURL(u.String())
  153. }