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 }