hotp.go raw
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 hotp
19
20 import (
21 "github.com/pquerna/otp"
22 "github.com/pquerna/otp/internal"
23 "io"
24
25 "crypto/hmac"
26 "crypto/rand"
27 "crypto/subtle"
28 "encoding/base32"
29 "encoding/binary"
30 "fmt"
31 "math"
32 "net/url"
33 "strings"
34 )
35
36 const debug = false
37
38 // Validate a HOTP passcode given a counter and secret.
39 // This is a shortcut for ValidateCustom, with parameters that
40 // are compataible with Google-Authenticator.
41 func Validate(passcode string, counter uint64, secret string) bool {
42 rv, _ := ValidateCustom(
43 passcode,
44 counter,
45 secret,
46 ValidateOpts{
47 Digits: otp.DigitsSix,
48 Algorithm: otp.AlgorithmSHA1,
49 },
50 )
51 return rv
52 }
53
54 // ValidateOpts provides options for ValidateCustom().
55 type ValidateOpts struct {
56 // Digits as part of the input. Defaults to 6.
57 Digits otp.Digits
58 // Algorithm to use for HMAC. Defaults to SHA1.
59 Algorithm otp.Algorithm
60 // Encoder to use for output code.
61 Encoder otp.Encoder
62 }
63
64 // GenerateCode creates a HOTP passcode given a counter and secret.
65 // This is a shortcut for GenerateCodeCustom, with parameters that
66 // are compataible with Google-Authenticator.
67 func GenerateCode(secret string, counter uint64) (string, error) {
68 return GenerateCodeCustom(secret, counter, ValidateOpts{
69 Digits: otp.DigitsSix,
70 Algorithm: otp.AlgorithmSHA1,
71 })
72 }
73
74 // GenerateCodeCustom uses a counter and secret value and options struct to
75 // create a passcode.
76 func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) {
77 //Set default value
78 if opts.Digits == 0 {
79 opts.Digits = otp.DigitsSix
80 }
81 // As noted in issue #10 and #17 this adds support for TOTP secrets that are
82 // missing their padding.
83 secret = strings.TrimSpace(secret)
84 if n := len(secret) % 8; n != 0 {
85 secret = secret + strings.Repeat("=", 8-n)
86 }
87
88 // As noted in issue #24 Google has started producing base32 in lower case,
89 // but the StdEncoding (and the RFC), expect a dictionary of only upper case letters.
90 secret = strings.ToUpper(secret)
91
92 secretBytes, err := base32.StdEncoding.DecodeString(secret)
93 if err != nil {
94 return "", otp.ErrValidateSecretInvalidBase32
95 }
96
97 buf := make([]byte, 8)
98 mac := hmac.New(opts.Algorithm.Hash, secretBytes)
99 binary.BigEndian.PutUint64(buf, counter)
100 if debug {
101 fmt.Printf("counter=%v\n", counter)
102 fmt.Printf("buf=%v\n", buf)
103 }
104
105 mac.Write(buf)
106 sum := mac.Sum(nil)
107
108 // "Dynamic truncation" in RFC 4226
109 // http://tools.ietf.org/html/rfc4226#section-5.4
110 offset := sum[len(sum)-1] & 0xf
111 value := int64(((int(sum[offset]) & 0x7f) << 24) |
112 ((int(sum[offset+1] & 0xff)) << 16) |
113 ((int(sum[offset+2] & 0xff)) << 8) |
114 (int(sum[offset+3]) & 0xff))
115
116 l := opts.Digits.Length()
117 switch opts.Encoder {
118 case otp.EncoderDefault:
119 mod := int32(value % int64(math.Pow10(l)))
120
121 if debug {
122 fmt.Printf("offset=%v\n", offset)
123 fmt.Printf("value=%v\n", value)
124 fmt.Printf("mod'ed=%v\n", mod)
125 }
126 passcode = opts.Digits.Format(mod)
127 case otp.EncoderSteam:
128 // Define the character set used by Steam Guard codes.
129 alphabet := []byte{
130 '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
131 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
132 'R', 'T', 'V', 'W', 'X', 'Y',
133 }
134 radix := int64(len(alphabet))
135
136 for i := 0; i < l; i++ {
137 digit := value % radix
138 value /= radix
139 c := alphabet[digit]
140 passcode += string(c)
141 }
142 }
143
144 return
145 }
146
147 // ValidateCustom validates an HOTP with customizable options. Most users should
148 // use Validate().
149 func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
150 passcode = strings.TrimSpace(passcode)
151
152 if len(passcode) != opts.Digits.Length() {
153 return false, otp.ErrValidateInputInvalidLength
154 }
155
156 otpstr, err := GenerateCodeCustom(secret, counter, opts)
157 if err != nil {
158 return false, err
159 }
160
161 if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 {
162 return true, nil
163 }
164
165 return false, nil
166 }
167
168 // GenerateOpts provides options for .Generate()
169 type GenerateOpts struct {
170 // Name of the issuing Organization/Company.
171 Issuer string
172 // Name of the User's Account (eg, email address)
173 AccountName string
174 // Size in size of the generated Secret. Defaults to 10 bytes.
175 SecretSize uint
176 // Secret to store. Defaults to a randomly generated secret of SecretSize. You should generally leave this empty.
177 Secret []byte
178 // Digits to request. Defaults to 6.
179 Digits otp.Digits
180 // Algorithm to use for HMAC. Defaults to SHA1.
181 Algorithm otp.Algorithm
182 // Reader to use for generating HOTP Key.
183 Rand io.Reader
184 }
185
186 var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
187
188 // Generate creates a new HOTP Key.
189 func Generate(opts GenerateOpts) (*otp.Key, error) {
190 // url encode the Issuer/AccountName
191 if opts.Issuer == "" {
192 return nil, otp.ErrGenerateMissingIssuer
193 }
194
195 if opts.AccountName == "" {
196 return nil, otp.ErrGenerateMissingAccountName
197 }
198
199 if opts.SecretSize == 0 {
200 opts.SecretSize = 10
201 }
202
203 if opts.Digits == 0 {
204 opts.Digits = otp.DigitsSix
205 }
206
207 if opts.Rand == nil {
208 opts.Rand = rand.Reader
209 }
210
211 // otpauth://hotp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
212
213 v := url.Values{}
214 if len(opts.Secret) != 0 {
215 v.Set("secret", b32NoPadding.EncodeToString(opts.Secret))
216 } else {
217 secret := make([]byte, opts.SecretSize)
218 _, err := io.ReadFull(opts.Rand, secret)
219 if err != nil {
220 return nil, err
221 }
222 v.Set("secret", b32NoPadding.EncodeToString(secret))
223 }
224
225 v.Set("issuer", opts.Issuer)
226 v.Set("algorithm", opts.Algorithm.String())
227 v.Set("digits", opts.Digits.String())
228
229 u := url.URL{
230 Scheme: "otpauth",
231 Host: "hotp",
232 Path: "/" + opts.Issuer + ":" + opts.AccountName,
233 RawQuery: internal.EncodeQuery(v),
234 }
235
236 return otp.NewKeyFromURL(u.String())
237 }
238