-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathotp.go
204 lines (173 loc) · 5.78 KB
/
otp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// Package otp provides the HMAC-based one-time password (HOTP) algorithm described in RFC 4226 and
// the time-based one time password (TOTP) algorithm described in RFC 6238.
package otp
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"hash"
"math"
)
// HashAlgorithm identifies the hash algorithm used for HMAC.
type HashAlgorithm int
const (
// HashAlgorithmSHA1 represents SHA1 algorithm.
HashAlgorithmSHA1 HashAlgorithm = iota
// HashAlgorithmSHA256 represents SHA256 algorithm.
HashAlgorithmSHA256
// HashAlgorithmSHA512 represents SHA512 algorithm.
HashAlgorithmSHA512
)
const (
// maxCodeDigits represents maximum digits of password code.
maxCodeDigits = 8
)
// hash gets the hash function specified by the algorithm enum.
func (algorithm HashAlgorithm) hash() (func() hash.Hash, error) {
switch algorithm {
case HashAlgorithmSHA1:
return sha1.New, nil
case HashAlgorithmSHA256:
return sha256.New, nil
case HashAlgorithmSHA512:
return sha512.New, nil
default:
return nil, errors.New("unknown hash algorithm")
}
}
// DefaultKeyByteSize gets the default value of HMAC key size in bytes.
func (algorithm HashAlgorithm) DefaultKeyByteSize() (int, error) {
switch algorithm {
case HashAlgorithmSHA1:
return 20, nil
case HashAlgorithmSHA256:
return 32, nil
case HashAlgorithmSHA512:
return 64, nil
default:
return 0, errors.New("unknown hash algorithm")
}
}
// generateSecret generates a new secret key.
func (algorithm HashAlgorithm) generateSecret() ([]byte, error) {
keyByteSize, _ := algorithm.DefaultKeyByteSize()
secret := make([]byte, keyByteSize)
_, err := rand.Read(secret)
if err != nil {
return nil, err
}
return secret, nil
}
// OTPManager represents an HMAC-based or time-based one-time password generator and validator.
type OTPManager interface {
// Generate generates the one-time password with the specified moving factor.
Generate(int64) string
// Validate validates whether the one-time password matches.
Validate(int64, string) bool
}
// hotpManager represents an HMAC-based one-time password (HOTP) generator and validator.
type hotpManager struct {
hashAlgorithm func() hash.Hash
secret []byte
codeDigits int
}
// NewHOTP creates a new HMAC-based one-time password (HOTP) manager with specified hash algorithm, secret keys and
// digit count of password codes.
//
// When provided secret key is nil, a new secret key will be generated with cryptographically secure pseudo-random
// number generator provided by the operation system. By default, length of the secret key is 20 bytes for SHA1
// algorithm, 32 bytes for SHA256 algorithm and 64 bytes for SHA512 algorithm.
//
// Code digit cannot be longer than 8 digits.
func NewHOTP(algorithm HashAlgorithm, secret []byte, codeDigit int) (OTPManager, error) {
var generator hotpManager
// Check algorithm
hashAlgorithm, err := algorithm.hash()
if err != nil {
return nil, err
}
generator.hashAlgorithm = hashAlgorithm
// Check secret key
if secret == nil {
generator.secret, err = algorithm.generateSecret()
if err != nil {
return nil, err
}
} else {
generator.secret = secret
}
// Check code digits
if codeDigit <= 0 || codeDigit > maxCodeDigits {
return nil, errors.New("invalid code digit")
}
generator.codeDigits = codeDigit
return &generator, nil
}
func (generator *hotpManager) Generate(movingFactor int64) string {
message := make([]byte, 8)
binary.BigEndian.PutUint64(message, uint64(movingFactor))
mac := hmac.New(generator.hashAlgorithm, generator.secret)
mac.Write(message)
hashResult := mac.Sum(nil)
offset := hashResult[len(hashResult)-1] & 0xf
truncated := binary.BigEndian.Uint32(hashResult[offset:offset+4]) & 0x7fffffff
code := truncated % uint32(math.Pow10(generator.codeDigits))
return fmt.Sprintf(fmt.Sprintf("%%0%dd", generator.codeDigits), code)
}
func (generator *hotpManager) Validate(movingFactor int64, code string) bool {
return generator.Generate(movingFactor) == code
}
// totpManager represents an time-based one-time password (HOTP) generator and validator.
type totpManager struct {
hotp *hotpManager
timeStep int
lookBackward int
lookForward int
}
// NewTOTP initializes a new time-based one-time password (TOTP) manager with specified hash algorithm, secret key,
// digit count of password codes, time step, and tolerant time steps.
//
// A new secret key will be generated if provided one is nil. Refers to NewHOTP function for details.
//
// Code digit cannot be longer than 8 digits.
//
// Tolerant time steps are only used for validating. These parameters can be used to allow certain clock drift
// between a client and the TOTP manager. Settings to 0 to accept no time drift at all.
func NewTOTP(algorithm HashAlgorithm, secret []byte, codeDigit, timeStep, lookBackward, lookForward int) (OTPManager, error) {
var generator totpManager
hotp, err := NewHOTP(algorithm, secret, codeDigit)
if err != nil {
return nil, err
}
generator.hotp = hotp.(*hotpManager)
if timeStep <= 0 {
return nil, errors.New("invalid time step")
}
generator.timeStep = timeStep
if lookBackward < 0 {
return nil, errors.New("invalid look-backward value")
}
generator.lookBackward = lookBackward
if lookForward < 0 {
return nil, errors.New("invalid look-forward value")
}
generator.lookForward = lookForward
return &generator, nil
}
func (generator *totpManager) Generate(epoch int64) string {
return generator.hotp.Generate(epoch / int64(generator.timeStep))
}
func (generator *totpManager) Validate(epoch int64, code string) bool {
for i := -generator.lookBackward; i <= generator.lookForward; i += 1 {
movingFactor := (epoch + int64(i*generator.timeStep)) / int64(generator.timeStep)
if generator.hotp.Generate(movingFactor) == code {
return true
}
}
return false
}