1
17
18 package hotp
19
20 import (
21 "github.com/pquerna/otp"
22 "io"
23
24 "crypto/hmac"
25 "crypto/rand"
26 "crypto/subtle"
27 "encoding/base32"
28 "encoding/binary"
29 "fmt"
30 "math"
31 "net/url"
32 "strings"
33 )
34
35 const debug = false
36
37
38
39
40 func Validate(passcode string, counter uint64, secret string) bool {
41 rv, _ := ValidateCustom(
42 passcode,
43 counter,
44 secret,
45 ValidateOpts{
46 Digits: otp.DigitsSix,
47 Algorithm: otp.AlgorithmSHA1,
48 },
49 )
50 return rv
51 }
52
53
54 type ValidateOpts struct {
55
56 Digits otp.Digits
57
58 Algorithm otp.Algorithm
59 }
60
61
62
63
64 func GenerateCode(secret string, counter uint64) (string, error) {
65 return GenerateCodeCustom(secret, counter, ValidateOpts{
66 Digits: otp.DigitsSix,
67 Algorithm: otp.AlgorithmSHA1,
68 })
69 }
70
71
72
73 func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) {
74
75
76 secret = strings.TrimSpace(secret)
77 if n := len(secret) % 8; n != 0 {
78 secret = secret + strings.Repeat("=", 8-n)
79 }
80
81
82
83 secret = strings.ToUpper(secret)
84
85 secretBytes, err := base32.StdEncoding.DecodeString(secret)
86 if err != nil {
87 return "", otp.ErrValidateSecretInvalidBase32
88 }
89
90 buf := make([]byte, 8)
91 mac := hmac.New(opts.Algorithm.Hash, secretBytes)
92 binary.BigEndian.PutUint64(buf, counter)
93 if debug {
94 fmt.Printf("counter=%v\n", counter)
95 fmt.Printf("buf=%v\n", buf)
96 }
97
98 mac.Write(buf)
99 sum := mac.Sum(nil)
100
101
102
103 offset := sum[len(sum)-1] & 0xf
104 value := int64(((int(sum[offset]) & 0x7f) << 24) |
105 ((int(sum[offset+1] & 0xff)) << 16) |
106 ((int(sum[offset+2] & 0xff)) << 8) |
107 (int(sum[offset+3]) & 0xff))
108
109 l := opts.Digits.Length()
110 mod := int32(value % int64(math.Pow10(l)))
111
112 if debug {
113 fmt.Printf("offset=%v\n", offset)
114 fmt.Printf("value=%v\n", value)
115 fmt.Printf("mod'ed=%v\n", mod)
116 }
117
118 return opts.Digits.Format(mod), nil
119 }
120
121
122
123 func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
124 passcode = strings.TrimSpace(passcode)
125
126 if len(passcode) != opts.Digits.Length() {
127 return false, otp.ErrValidateInputInvalidLength
128 }
129
130 otpstr, err := GenerateCodeCustom(secret, counter, opts)
131 if err != nil {
132 return false, err
133 }
134
135 if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 {
136 return true, nil
137 }
138
139 return false, nil
140 }
141
142
143 type GenerateOpts struct {
144
145 Issuer string
146
147 AccountName string
148
149 SecretSize uint
150
151 Secret []byte
152
153 Digits otp.Digits
154
155 Algorithm otp.Algorithm
156
157 Rand io.Reader
158 }
159
160 var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
161
162
163 func Generate(opts GenerateOpts) (*otp.Key, error) {
164
165 if opts.Issuer == "" {
166 return nil, otp.ErrGenerateMissingIssuer
167 }
168
169 if opts.AccountName == "" {
170 return nil, otp.ErrGenerateMissingAccountName
171 }
172
173 if opts.SecretSize == 0 {
174 opts.SecretSize = 10
175 }
176
177 if opts.Digits == 0 {
178 opts.Digits = otp.DigitsSix
179 }
180
181 if opts.Rand == nil {
182 opts.Rand = rand.Reader
183 }
184
185
186
187 v := url.Values{}
188 if len(opts.Secret) != 0 {
189 v.Set("secret", b32NoPadding.EncodeToString(opts.Secret))
190 } else {
191 secret := make([]byte, opts.SecretSize)
192 _, err := opts.Rand.Read(secret)
193 if err != nil {
194 return nil, err
195 }
196 v.Set("secret", b32NoPadding.EncodeToString(secret))
197 }
198
199 v.Set("issuer", opts.Issuer)
200 v.Set("algorithm", opts.Algorithm.String())
201 v.Set("digits", opts.Digits.String())
202
203 u := url.URL{
204 Scheme: "otpauth",
205 Host: "hotp",
206 Path: "/" + opts.Issuer + ":" + opts.AccountName,
207 RawQuery: v.Encode(),
208 }
209
210 return otp.NewKeyFromURL(u.String())
211 }
212
View as plain text