1
17
18 package totp
19
20 import (
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
34
35
36 func 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
52
53
54 func 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
64 type ValidateOpts struct {
65
66 Period uint
67
68
69
70 Skew uint
71
72 Digits otp.Digits
73
74 Algorithm otp.Algorithm
75 }
76
77
78
79
80 func 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
96
97 func 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
130
131 type GenerateOpts struct {
132
133 Issuer string
134
135 AccountName string
136
137 Period uint
138
139 SecretSize uint
140
141 Secret []byte
142
143 Digits otp.Digits
144
145 Algorithm otp.Algorithm
146
147 Rand io.Reader
148 }
149
150 var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
151
152
153 func Generate(opts GenerateOpts) (*otp.Key, error) {
154
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
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
View as plain text