TL;DR
Если вы когда-нибудь замечали, что длинные коды двухфакторной аутентификации (TOTP) часто содержат повторы вроде 131488
или симметрии вроде 1221
— это не баг, а статистическая закономерность. Чем длиннее код, тем выше вероятность, что в нём встретятся простые или "запоминающиеся" фрагменты. Это нормально и не снижает безопасность.
Базовая идея
TOTP-коды генерируются по стандарту RFC 6238: берётся криптографический HMAC от текущего времени и секретного ключа. Итоговая 6- или 8-значная строка — результат детерминированной, но непредсказуемой функции.
Секрет K
+ время T
→ HMAC(K, T)
→ усечённый до 6 цифр код.
Почему повторы встречаются чаще, чем кажется
1. Вероятность совпадений при фиксированном алфавите
6 цифр, 10 возможных значений (0–9): всего 1 000 000 комбинаций. Но без повторяющихся цифр — только ~150 000. Значит, в 85% случаев в коде есть хотя бы одна повторяющаяся цифра.
Чем длиннее код, тем выше вероятность:
повторов (
44
,000
,878
)последовательностей (
123
,321
)симметрий (
2442
,9009
)
Это описывается через парадокс дней рождения и законы теории информации.
2. Эффект Рамсея: паттерны появляются неизбежно
Согласно идеям теории Рамсея, любая достаточно длинная случайная строка почти гарантированно содержит локально "организованные" подстроки — даже если глобально она абсолютно случайна.
Безопасность не страдает
Появление симметрии или повтора не делает код менее защищённым. Ключевые свойства сохраняются:
Секретный ключ
K
остаётся криптостойкимХэш-функция (обычно SHA-1 или SHA-256) односторонняя
Код живёт 30 секунд, что исключает эффективный перебор
Если кто-то хочет "делать коды более красивыми" — это уже вмешательство в криптографию и снижение энтропии. Так делать не стоит.
Вывод
Запоминающиеся паттерны в TOTP-кодах — это не слабость, а статистическое следствие. Чем длиннее код, тем больше в нём места для "совпадений". Это подтверждает, что генерация работает корректно — а не наоборот.
P.S. Пример на Python для тех, кто хочет экспериментировать
import time, hmac, base64, hashlib
def totp(secret, digits=6, interval=30):
key = base64.b32decode(secret.upper())
counter = int(time.time() / interval)
msg = counter.to_bytes(8, 'big')
h = hmac.new(key, msg, hashlib.sha1).digest()
o = h[-1] & 0x0F
code = (int.from_bytes(h[o:o+4], 'big') & 0x7fffffff) % (10**digits)
return str(code).zfill(digits)
print(totp('JBSWY3DPEHPK3PXP'))