Initial commit: hotpcalc CLI
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user