88 lines
2.7 KiB
Go
88 lines
2.7 KiB
Go
package otp
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
// Match — найденная позиция токена. Counter соответствует ПЕРВОМУ из двух
|
|
// введённых кодов; второй код приходится на Counter+1.
|
|
type Match struct {
|
|
Counter uint64
|
|
Time time.Time // начало окна первого кода; заполняется только для TOTP
|
|
}
|
|
|
|
// ErrNotFound возвращается, когда пара кодов не найдена в заданном диапазоне.
|
|
type ErrNotFound struct {
|
|
From, To uint64
|
|
Mode string
|
|
}
|
|
|
|
func (e *ErrNotFound) Error() string {
|
|
return fmt.Sprintf("пара кодов не найдена в диапазоне %s %d..%d", e.Mode, e.From, e.To)
|
|
}
|
|
|
|
// FindCounter ищет значение счётчика HOTP, при котором два введённых кода
|
|
// идут подряд. Перебираются счётчики [start, start+span).
|
|
func FindCounter(c Config, otp1, otp2 string, start, span uint64) ([]Match, error) {
|
|
if err := c.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if span < 2 {
|
|
return nil, fmt.Errorf("диапазон перебора должен быть не меньше 2")
|
|
}
|
|
end := start + span - 1 // включительно; защита от переполнения ниже
|
|
if end < start {
|
|
end = math.MaxUint64
|
|
}
|
|
|
|
var matches []Match
|
|
prev := c.HOTP(start)
|
|
for i := start; i < end; i++ {
|
|
cur := c.HOTP(i + 1)
|
|
if equalCode(prev, otp1) && equalCode(cur, otp2) {
|
|
matches = append(matches, Match{Counter: i})
|
|
}
|
|
prev = cur
|
|
}
|
|
if len(matches) == 0 {
|
|
return nil, &ErrNotFound{From: start, To: end, Mode: "счётчика"}
|
|
}
|
|
return matches, nil
|
|
}
|
|
|
|
// FindTimeStep ищет временной шаг TOTP, при котором два введённых кода идут
|
|
// подряд. Перебор идёт вокруг момента now в пределах ±window.
|
|
func FindTimeStep(c Config, otp1, otp2 string, now time.Time, window time.Duration) ([]Match, error) {
|
|
if err := c.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if window < 0 {
|
|
return nil, fmt.Errorf("окно поиска не может быть отрицательным")
|
|
}
|
|
|
|
center := c.TimeStep(now)
|
|
half := uint64(window / c.Step)
|
|
|
|
start := uint64(0)
|
|
if center > half {
|
|
start = center - half
|
|
}
|
|
span := half*2 + 2 // +2, чтобы центр и обе границы попали в пары
|
|
|
|
matches, err := FindCounter(c, otp1, otp2, start, span)
|
|
if err != nil {
|
|
var nf *ErrNotFound
|
|
if errors.As(err, &nf) {
|
|
nf.Mode = "временного шага"
|
|
}
|
|
return nil, err
|
|
}
|
|
for i := range matches {
|
|
matches[i].Time = c.StepStart(matches[i].Counter)
|
|
}
|
|
return matches, nil
|
|
}
|