Initial commit: hotpcalc CLI

This commit is contained in:
2026-09-20 20:04:18 +03:00
commit 8f42dc690e
24 changed files with 5182 additions and 0 deletions
+87
View File
@@ -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
}