hotp.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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. // As noted in issue #10 this adds support for TOTP secrets that are
  66. // missing their padding.
  67. if n := len(secret) % 8; n != 0 {
  68. secret = secret + strings.Repeat("=", 8-n)
  69. }
  70. secretBytes, err := base32.StdEncoding.DecodeString(secret)
  71. if err != nil {
  72. return "", otp.ErrValidateSecretInvalidBase32
  73. }
  74. buf := make([]byte, 8)
  75. mac := hmac.New(opts.Algorithm.Hash, secretBytes)
  76. binary.BigEndian.PutUint64(buf, counter)
  77. if debug {
  78. fmt.Printf("counter=%v\n", counter)
  79. fmt.Printf("buf=%v\n", buf)
  80. }
  81. mac.Write(buf)
  82. sum := mac.Sum(nil)
  83. // "Dynamic truncation" in RFC 4226
  84. // http://tools.ietf.org/html/rfc4226#section-5.4
  85. offset := sum[len(sum)-1] & 0xf
  86. value := int64(((int(sum[offset]) & 0x7f) << 24) |
  87. ((int(sum[offset+1] & 0xff)) << 16) |
  88. ((int(sum[offset+2] & 0xff)) << 8) |
  89. (int(sum[offset+3]) & 0xff))
  90. l := opts.Digits.Length()
  91. mod := int32(value % int64(math.Pow10(l)))
  92. if debug {
  93. fmt.Printf("offset=%v\n", offset)
  94. fmt.Printf("value=%v\n", value)
  95. fmt.Printf("mod'ed=%v\n", mod)
  96. }
  97. return opts.Digits.Format(mod), nil
  98. }
  99. // ValidateCustom validates an HOTP with customizable options. Most users should
  100. // use Validate().
  101. func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
  102. passcode = strings.TrimSpace(passcode)
  103. if len(passcode) != opts.Digits.Length() {
  104. return false, otp.ErrValidateInputInvalidLength
  105. }
  106. otpstr, err := GenerateCodeCustom(secret, counter, opts)
  107. if err != nil {
  108. return false, err
  109. }
  110. if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 {
  111. return true, nil
  112. }
  113. return false, nil
  114. }
  115. // GenerateOpts provides options for .Generate()
  116. type GenerateOpts struct {
  117. // Name of the issuing Organization/Company.
  118. Issuer string
  119. // Name of the User's Account (eg, email address)
  120. AccountName string
  121. // Size in size of the generated Secret. Defaults to 10 bytes.
  122. SecretSize uint
  123. // Digits to request. Defaults to 6.
  124. Digits otp.Digits
  125. // Algorithm to use for HMAC. Defaults to SHA1.
  126. Algorithm otp.Algorithm
  127. }
  128. // Generate creates a new HOTP Key.
  129. func Generate(opts GenerateOpts) (*otp.Key, error) {
  130. // url encode the Issuer/AccountName
  131. if opts.Issuer == "" {
  132. return nil, otp.ErrGenerateMissingIssuer
  133. }
  134. if opts.AccountName == "" {
  135. return nil, otp.ErrGenerateMissingAccountName
  136. }
  137. if opts.SecretSize == 0 {
  138. opts.SecretSize = 10
  139. }
  140. // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
  141. v := url.Values{}
  142. secret := make([]byte, opts.SecretSize)
  143. _, err := rand.Read(secret)
  144. if err != nil {
  145. return nil, err
  146. }
  147. v.Set("secret", base32.StdEncoding.EncodeToString(secret))
  148. v.Set("issuer", opts.Issuer)
  149. v.Set("algorithm", opts.Algorithm.String())
  150. v.Set("digits", opts.Digits.String())
  151. u := url.URL{
  152. Scheme: "otpauth",
  153. Host: "hotp",
  154. Path: "/" + opts.Issuer + ":" + opts.AccountName,
  155. RawQuery: v.Encode(),
  156. }
  157. return otp.NewKeyFromURL(u.String())
  158. }