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