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 18 package totp
19 20 import (
21 "github.com/pquerna/otp"
22 "github.com/pquerna/otp/hotp"
23 "github.com/pquerna/otp/internal"
24 "io"
25 26 "crypto/rand"
27 "encoding/base32"
28 "math"
29 "net/url"
30 "strconv"
31 "time"
32 )
33 34 // Validate a TOTP using the current time.
35 // A shortcut for ValidateCustom, Validate uses a configuration
36 // that is compatible with Google-Authenticator and most clients.
37 func Validate(passcode string, secret string) bool {
38 rv, _ := ValidateCustom(
39 passcode,
40 secret,
41 time.Now().UTC(),
42 ValidateOpts{
43 Period: 30,
44 Skew: 1,
45 Digits: otp.DigitsSix,
46 Algorithm: otp.AlgorithmSHA1,
47 },
48 )
49 return rv
50 }
51 52 // GenerateCode creates a TOTP token using the current time.
53 // A shortcut for GenerateCodeCustom, GenerateCode uses a configuration
54 // that is compatible with Google-Authenticator and most clients.
55 func GenerateCode(secret string, t time.Time) (string, error) {
56 return GenerateCodeCustom(secret, t, ValidateOpts{
57 Period: 30,
58 Skew: 1,
59 Digits: otp.DigitsSix,
60 Algorithm: otp.AlgorithmSHA1,
61 })
62 }
63 64 // ValidateOpts provides options for ValidateCustom().
65 type ValidateOpts struct {
66 // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
67 Period uint
68 // Periods before or after the current time to allow. Value of 1 allows up to Period
69 // of either side of the specified time. Defaults to 0 allowed skews. Values greater
70 // than 1 are likely sketchy.
71 Skew uint
72 // Digits as part of the input. Defaults to 6.
73 Digits otp.Digits
74 // Algorithm to use for HMAC. Defaults to SHA1.
75 Algorithm otp.Algorithm
76 // Encoder to use for output code.
77 Encoder otp.Encoder
78 }
79 80 // GenerateCodeCustom takes a timepoint and produces a passcode using a
81 // secret and the provided opts. (Under the hood, this is making an adapted
82 // call to hotp.GenerateCodeCustom)
83 func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) {
84 if opts.Period == 0 {
85 opts.Period = 30
86 }
87 counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
88 passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{
89 Digits: opts.Digits,
90 Algorithm: opts.Algorithm,
91 Encoder: opts.Encoder,
92 })
93 if err != nil {
94 return "", err
95 }
96 return passcode, nil
97 }
98 99 // ValidateCustom validates a TOTP given a user specified time and custom options.
100 // Most users should use Validate() to provide an interpolatable TOTP experience.
101 func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) {
102 if opts.Period == 0 {
103 opts.Period = 30
104 }
105 106 counters := []uint64{}
107 counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period)))
108 109 counters = append(counters, uint64(counter))
110 for i := 1; i <= int(opts.Skew); i++ {
111 counters = append(counters, uint64(counter+int64(i)))
112 counters = append(counters, uint64(counter-int64(i)))
113 }
114 115 for _, counter := range counters {
116 rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{
117 Digits: opts.Digits,
118 Algorithm: opts.Algorithm,
119 Encoder: opts.Encoder,
120 })
121 if err != nil {
122 return false, err
123 }
124 125 if rv == true {
126 return true, nil
127 }
128 }
129 130 return false, nil
131 }
132 133 // GenerateOpts provides options for Generate(). The default values
134 // are compatible with Google-Authenticator.
135 type GenerateOpts struct {
136 // Name of the issuing Organization/Company.
137 Issuer string
138 // Name of the User's Account (eg, email address)
139 AccountName string
140 // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
141 Period uint
142 // Size in size of the generated Secret. Defaults to 20 bytes.
143 SecretSize uint
144 // Secret to store. Defaults to a randomly generated secret of SecretSize. You should generally leave this empty.
145 Secret []byte
146 // Digits to request. Defaults to 6.
147 Digits otp.Digits
148 // Algorithm to use for HMAC. Defaults to SHA1.
149 Algorithm otp.Algorithm
150 // Reader to use for generating TOTP Key.
151 Rand io.Reader
152 }
153 154 var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
155 156 // Generate a new TOTP Key.
157 func Generate(opts GenerateOpts) (*otp.Key, error) {
158 // url encode the Issuer/AccountName
159 if opts.Issuer == "" {
160 return nil, otp.ErrGenerateMissingIssuer
161 }
162 163 if opts.AccountName == "" {
164 return nil, otp.ErrGenerateMissingAccountName
165 }
166 167 if opts.Period == 0 {
168 opts.Period = 30
169 }
170 171 if opts.SecretSize == 0 {
172 opts.SecretSize = 20
173 }
174 175 if opts.Digits == 0 {
176 opts.Digits = otp.DigitsSix
177 }
178 179 if opts.Rand == nil {
180 opts.Rand = rand.Reader
181 }
182 183 // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
184 185 v := url.Values{}
186 if len(opts.Secret) != 0 {
187 v.Set("secret", b32NoPadding.EncodeToString(opts.Secret))
188 } else {
189 secret := make([]byte, opts.SecretSize)
190 _, err := io.ReadFull(opts.Rand, secret)
191 if err != nil {
192 return nil, err
193 }
194 v.Set("secret", b32NoPadding.EncodeToString(secret))
195 }
196 197 v.Set("issuer", opts.Issuer)
198 v.Set("period", strconv.FormatUint(uint64(opts.Period), 10))
199 v.Set("algorithm", opts.Algorithm.String())
200 v.Set("digits", opts.Digits.String())
201 202 u := url.URL{
203 Scheme: "otpauth",
204 Host: "totp",
205 Path: "/" + opts.Issuer + ":" + opts.AccountName,
206 RawQuery: internal.EncodeQuery(v),
207 }
208 209 return otp.NewKeyFromURL(u.String())
210 }
211