-
Notifications
You must be signed in to change notification settings - Fork 0
/
time_based.go
146 lines (117 loc) · 3.43 KB
/
time_based.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
package otp
import (
"crypto/subtle"
"errors"
"fmt"
"math"
"sync"
"time"
)
var ErrPasswordUsed = errors.New("the provided password has already been used")
var usedTOTPs = struct {
sync.RWMutex
data map[string]time.Time
}{data: make(map[string]time.Time)}
// CleanUsedTOTP will clear any recorded used TOTP that matches
// the given string.
func CleanUsedTOTP(password string) {
usedTOTPs.Lock()
defer usedTOTPs.Unlock()
delete(usedTOTPs.data, password)
}
// CleanUsedTOTPs will clear all recorded used TOTPs that are older than the
// given age duration.
func CleanUsedTOTPs(age time.Duration) {
usedTOTPs.Lock()
defer usedTOTPs.Unlock()
for password, t := range usedTOTPs.data {
if time.Since(t) > age {
delete(usedTOTPs.data, password)
}
}
}
// TimeBased implements a "Time-based One Time Password" (TOTP) in accordance
// with RFC6238 and any errata.
type TimeBased struct {
hmacBased HMACBased
baseTime time.Time
timeStep time.Duration
}
func NewTimeBased(digits int, alg Algorithm, baseTime time.Time, timeStep time.Duration) (TimeBased, error) {
otp := TimeBased{
baseTime: baseTime,
timeStep: timeStep,
}
hmacBased, err := NewHMACBased(digits, alg)
if err != nil {
return otp, fmt.Errorf("new HMAC based: %w", err)
}
otp.hmacBased = hmacBased
if otp.baseTime.IsZero() || otp.baseTime.Unix() < 0 {
return otp, errors.New("base time must be set to at least the unix epoch (0)")
}
if otp.timeStep.Seconds() < 30.0 {
return otp, errors.New("time step must be at least 30 seconds")
}
return otp, nil
}
func (otp TimeBased) Generate(key []byte, t time.Time) (string, error) {
if t.IsZero() || t.Unix() < 0 {
return "", errors.New("time must be set to at least the unix epoch (0)")
}
count := uint64(math.Floor(float64(t.Unix()-otp.baseTime.Unix()) / otp.timeStep.Seconds()))
totp, err := otp.hmacBased.Generate(key, count)
if err != nil {
return "", err
}
return totp, nil
}
func (otp TimeBased) Check(key []byte, t time.Time, delaySteps int, userPassword string) (bool, error) {
if delaySteps < 0 {
return false, errors.New("delay steps cannot be negative")
}
if wantMax := 4; delaySteps > wantMax {
return false, fmt.Errorf("delay steps is too large, a maximum of %v is expected", wantMax)
}
if len(key) == 0 || len(userPassword) == 0 {
return false, nil
}
defer CleanUsedTOTPs(10 * time.Minute)
usedTOTPs.RLock()
if _, ok := usedTOTPs.data[userPassword]; ok {
usedTOTPs.RUnlock()
return false, ErrPasswordUsed
}
usedTOTPs.RUnlock()
// Check into the past
for i := range int(delaySteps) + 1 {
step := otp.timeStep * time.Duration(i)
password, err := otp.Generate(key, t.Add(-step))
if err != nil {
return false, fmt.Errorf("generate: %w", err)
}
if subtle.ConstantTimeCompare([]byte(password), []byte(userPassword)) == 1 {
usedTOTPs.Lock()
usedTOTPs.data[password] = time.Now()
usedTOTPs.Unlock()
return true, nil
}
}
// Check into the future
// We start at index 1 here because the checks into the past already include
// the current time (i = 0)
for i := 1; i <= int(delaySteps); i++ {
step := otp.timeStep * time.Duration(i)
password, err := otp.Generate(key, t.Add(step))
if err != nil {
return false, fmt.Errorf("generate delayed: %w", err)
}
if subtle.ConstantTimeCompare([]byte(password), []byte(userPassword)) == 1 {
usedTOTPs.Lock()
usedTOTPs.data[password] = time.Now()
usedTOTPs.Unlock()
return true, nil
}
}
return false, nil
}