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.

hotp.go 5.4KB

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