Начните с вывода: Math.random() — обычный псевдослучайный генератор. MDN прямо указывает, что он не даёт криптографически стойких чисел и не должен использоваться для чего-либо, связанного с безопасностью. Пароли для входа, одноразовые токены и ключи требуют crypto.getRandomValues(). Хаотичная строка, кнопка «Сгенерировать», работающая офлайн, и замок в адресной строке — каждый факт доказывает свою границу. Ни один из них не отвечает на вопрос «какой API вызвал Сгенерировать».
Ответ на поисковый запрос в одном предложении
Math.random() возвращает число с плавающей точкой в диапазоне [0, 1) с примерно равномерным распределением. MDN формулирует жёстко: это значение не является криптографически стойким; вместо него используйте Crypto.getRandomValues(). «Выглядит достаточно хаотично» описывает смесь символов на выходе. Это ничего не говорит о том, сможет ли наблюдатель, увидев несколько результатов — или исходный код страницы — восстановить внутреннее состояние генератора и вывести следующие кандидаты.
Эта заметка для разработчиков и администраторов, которые проверяют «онлайн-генератор паролей», и для всех, кому коллега прислал вкладку «Сгенерировать», которую хочется осмотреть перед использованием. Это не инструкция к генератору паролей и не повторяет обход Network на тему «уходил ли открытый текст из этой вкладки». Вы должны унести слой, который отделяет обычный PRNG от CSPRNG, почему наивный modulo может смещать даже хороший API, и точный путь от клика «Сгенерировать» вниз по стеку вызовов.
«Хаотичный вид» означает лишь то, что символы могут быть смешаны
Судить о «случайности» на глаз дёшево и обычно ошибочно. aB3$kP9x и Tr0ub4dor&3 оба похожи на пароли. Первый может идти из широкого набора символов. Второй — из одной запоминающейся фразы с типичными подстановками. Внешний вид отвечает на «похоже ли это на пароль», а не на «можно ли вычислить следующий бит».
Даже когда набор действительно включает верхний и нижний регистр, цифры и символы, предсказуемая последовательность индексов всё равно сужает задачу. Атакующий, который может перезапустить тот же генератор — или восстановить его состояние после нескольких наблюдаемых выходов — перебирает оставшихся кандидатов вместо полного пространства. Для пароля входа непредсказуемость важнее «есть спецсимвол».
Длина — отдельный вопрос. Один и тот же CSPRNG на 6 и на 16 символов отличается на порядки по стоимости полного перебора. Меньше 8 — малое пространство даже при правильном API. В случайном режиме FastPwd допускает 6–128 символов, по умолчанию 16; короче 8 помечается как более слабая защита — это предупреждение о длине, а не о более слабом алгоритме. Держите два утверждения раздельно: API отвечает за непредсказуемость; длина — за размер пространства поиска.
Какой случайностью на самом деле является Math.random
ECMAScript определяет Math.random() как Number больше или равный 0 и меньше 1, полученный с помощью реализационно-зависимого алгоритма или стратегии с примерно равномерным распределением. Спецификация языка не требует устойчивости к предсказанию и не требует, чтобы seed брался из пула энтропии ОС. Движок может выбрать PRNG с отличной статистикой и обратимым состоянием.
Движок V8 в Chrome до конца 2015 года использовал MWC1616. Собственный разбор V8 фиксирует ограничения: 64 бита внутреннего состояния, смещённое качество и провалы в TestU01. С V8 4.9.41 (в составе Chrome 49) перешли на xorshift128+: 128 бит состояния, период 2^128 − 1 и проход TestU01. Firefox и Safari позже перешли на то же семейство. Это статистические улучшения, а не апгрейд безопасности.
Тот же пост V8 проводит границу одной фразой: xorshift128+ намного лучше старой реализации и всё ещё не является криптографически стойким. Хеширование, подписи и шифрование/расшифрование — не работа для обычного PRNG; им занимается Web Cryptography API через window.crypto.getRandomValues. «Выглядит статистически хаотично» и «наблюдатель не может дёшево восстановить последующие выходы» — две разные планки. Паролю нужна вторая.
Запрос CodeQL Insecure randomness на GitHub трактует использование Math.random для выпуска пароля как находку по той же причине: если атакующий может предсказать значение, он может войти как новый пользователь. Генератор, который только вызывает Math.random() и проецирует на набор символов, может пройти визуальную проверку и грубый частотный тест, а затем провалить ревью, где предполагают, что кто-то читал исходник или перехватил несколько выходов. Не пишите «использовали Math.random, но набор широкий» как зачёт.
Что на самом деле меняет getRandomValues
crypto.getRandomValues(typedArray) заполняет типизированный массив целых — Uint8Array, Uint32Array и другие целочисленные представления — значениями, случайными в криптографическом смысле. MDN явно описывает компромисс реализации: ради производительности браузеры обычно не читают аппаратный RNG на каждый бит. Они инициализируют PRNG, пригодный для криптографии, из источника энтропии ОС — например Unix /dev/urandom или системного интерфейса случайных чисел Windows. Алгоритм может меняться между браузерами. Цель проектирования — нет: опубликованный выход не должен позволять дёшево восстановить внутреннее состояние.
Два API живут в разных спецификациях. Math.random — ECMA-262. getRandomValues — Web Cryptography. Один вызов больше 65 536 байт бросает QuotaExceededError; пароль из 16 символов далеко от этого предела. getRandomValues — единственный метод Crypto, который может работать в небезопасном контексте; вывод ключей и subtle.encrypt по-прежнему требуют безопасного контекста (HTTPS на продакшен-странице). «Работает по HTTP» — не повод отказываться от TLS.
Разделите «эта вкладка нарисовала случайные байты» и «сервер нарисовал и вернул их». Даже если сервер использовал качественный CSPRNG, пароль уже побывал в памяти другой стороны, в логах и в этом HTTP-обмене — как открытый текст или как что-то обратимое. Замок в адресной строке шифрует только участок от этого браузера до текущей TLS-конечной точки. Это не значит, что конечная точка не может прочитать тело. Если страница заявляет «сгенерировано локально», «Сгенерировать» не должен POST-ить пароль наружу и получать его обратно.
| Что вы увидели | Что это может подтвердить | Что это не может подтвердить |
|---|---|---|
| Выход выглядит хаотично | Набор символов может быть широким | Процесс непредсказуем |
В исходнике есть Math.random |
Страница где-то использует обычный PRNG | «Сгенерировать» использует его для пароля |
В исходнике есть getRandomValues |
Страница импортировала CSPRNG API | Этот клик использовал этот API |
| «Сгенерировать» работает офлайн | Этот клик не ждал живой API для выпуска | Исходник — не Math.random |
| Поиск в Network не находит пароль | В этом наблюдении пароль не ушёл как рабочие данные | Случайный API — CSPRNG |
Смещение modulo: CSPRNG всё равно можно исказить
Вторая частая ошибка возникает после того, как скрипт уже вызвал getRandomValues. Берут байт из диапазона 0–255 и пишут charset[byte % charset.length]. Если длина набора не делит 256, первые символы получают лишнюю долю остатка и встречаются чуть чаще. На 16-символьном пароле перекос обычно мал, но он показывает, что реализация не довела «равномерность» до конца. Нестандартный набор или проекция 8 бит на большой словарь делают смещение заметнее.
Rejection sampling исправляет это: вычислите наибольший кратный длине набора, который всё ещё помещается в 256 (или 2^32), отбрасывайте любой draw на пределе или выше, затем берите modulo. Случайные символы и читаемые фразы FastPwd оба вызывают crypto.getRandomValues, берут 32-битное целое через Uint32Array(1), задают предел Math.floor(0x100000000 / max) * max и зацикливаются, пока draw попадает в хвост. Если getRandomValues недоступен, генерация отклоняется — запасного Math.random нет. Проверяя другой сайт, читайте дальше «нарисовать целое → отбросить хвост → сопоставить символ». Найти имя функции — не конец ревью.
Не тестируйте на реальном пароле входа, боевом API-ключе или незамаскированном тикете. Сгенерируйте одноразовую строку и выбросьте её. Вы проверяете путь «Сгенерировать» и трафик, а не раскрываете новый секрет второй раз.
Проверьте сейчас: исходник, консоль, Network
Слоган — не доказательство. Подготовьте маркер, который никогда не попадёт в реальный трафик: установите длину 16, сгенерируйте один раз и считайте полную строку канарейкой. Три шага ниже заканчиваются в этой вкладке. Аккаунт не нужен.
- Откройте DevTools → Sources или «Просмотр кода страницы». Ищите отдельно
Math.randomиgetRandomValues. Запишите файлы и имена функций. Пока не делайте вывод. - Читайте вниз от обработчика клика кнопки «Сгенерировать», пока символы не склеиваются в результат. Этот путь должен вызывать
crypto.getRandomValues. Если видите толькоMath.random(),Date.now()или фиксированный seed — эта генерация не CSPRNG. - Перейдите в Network, включите Preserve log, оставьте фильтр All. Сгенерируйте ещё раз. Вставьте полную строку в поиск. Строка запроса, query, тело запроса и payload аналитики не должны содержать этот пароль.
Консоль доказывает лишь наличие API
В Console выражение typeof crypto.getRandomValues === "function", возвращающее true, означает только то, что браузер этого документа экспонирует метод. Это не значит, что скрипт страницы его вызвал. Запустить одну строку getRandomValues самому и считать пароль страницы «уже проверенным» — два несвязанных фрагмента.
Офлайн доказывает лишь отсутствие удалённого выпуска
Дождитесь полной загрузки страницы. Затем поставьте Network в Offline или отключите сеть системы и сгенерируйте снова. Если результат всё равно появился, этот клик не зависел от живой API. Это сильный сигнал «сгенерировано в этой вкладке». Math.random тоже не требует сети. Offline — зачёт, который всё равно нужно сопоставить с путём в исходнике и поиском канарейки.
В бандлах имена часто сжаты до одной буквы. Если getRandomValues не находится, ищите crypto и Uint32Array и смотрите, появляется ли после «Сгенерировать» новый рабочий POST. Минификация — не освобождение от проверки. Это лишь один дополнительный шаг поиска.
Три способа переоценить «случайность»
Первый — считать замок в адресной строке доказательством CSPRNG. HTTPS / TLS защищает участок от этого браузера до текущей TLS-конечной точки. Сервер, который получил POST с паролем, тоже показывает замок. Замок не отвечает на Math.random против getRandomValues.
Второй — считать «в бандле упоминается getRandomValues» доказательством, что «Сгенерировать» использовал CSPRNG. Тот же файл может использовать Web Crypto для шифрования файлов, burn-ссылки или демо, а путь пароля всё ещё вызывает Math.random. И наоборот: Math.random в рекламе или анимации не опровергает путь пароля. Стек клика — это доказательство.
Третий — считать «не загрузилось» синонимом «непредсказуемо». Чистая панель Network означает лишь то, что в этом наблюдении пароль не покинул вкладку как HTTP-поле с рабочими данными. Расширение всё ещё может читать DOM. История буфера обмена ОС может сохранить строку, которую вы скопировали. Это другие границы. Качество случайности, факт загрузки и то, осталась ли копия после ухода — три отдельных вопроса.
Частые вопросы
Если сгенерированный пароль выглядит достаточно хаотичным, можно ли использовать его для входа?
Не по одному виду. Смешанные символы лишь намекают на широкий набор. Они не доказывают непредсказуемость процесса. MDN указывает: Math.random нельзя применять для безопасности. Дополнительно проверьте достаточность длины, повторное использование на других сайтах и вызывает ли «Сгенерировать» crypto.getRandomValues.
Если страница всё ещё генерирует пароль офлайн, значит ли это, что случайность безопасна?
Это доказывает лишь то, что этот клик не ждал живой API и сервер не выпустил строку за вас. Math.random тоже работает офлайн. После успешного Offline в пути исходника всё равно должен быть getRandomValues, а в Network не должно быть пароля, который вы только что создали.
В исходнике страницы есть и Math.random, и getRandomValues. Можно ли ею пользоваться?
Зависит от функции, которую вызывает кнопка «Сгенерировать». Анимации и порядок перемешивания могут использовать Math.random. Пароли, токены и ключи обязаны идти через getRandomValues. Откройте Sources и читайте вниз от обработчика клика. Не останавливайтесь на количестве вхождений каждого имени в бандле.
Какой API случайности использует FastPwd, и загружается ли пароль?
Случайные символы и читаемые фразы оба вызывают crypto.getRandomValues и проецируют 32-битное целое на набор символов с rejection sampling, чтобы отбросить хвост modulo. Если API недоступен, генерация отклоняется. Длина 6–128 символов, по умолчанию 16; короче 8 помечается как более слабая. Результат по умолчанию не загружается. Открыл — и пользуйся, без аккаунта.
Пройдите те же шаги с одноразовым паролем в готовом инструменте
Если нужна страница, где граница случайности описана открыто, откройте генератор паролей FastPwd. Регистрация не нужна. Оставьте случайный режим на 16 символов по умолчанию и сгенерируйте одноразовую строку. Повторите раздел пять: в Sources читайте от «Сгенерировать» вниз до getRandomValues и цикла rejection sampling; в Network ищите полную строку, чтобы строка запроса и тело её не содержали.
Затем, если хотите узнать, похожа ли строка на широко злоупотребляемый пароль, возьмите другую одноразовую строку в Проверку пароля и сверьте с публичным списком высокой частоты. Проверяемый пароль не загружается. Промах не доказывает, что пароль никогда не светился в сети; попадание лишь означает, что он слишком распространён. Это граница списка слабых паролей, а не CSPRNG. Не вставляйте пароль, который собираетесь включить, в любой онлайн-чекер.
Перед следующей незнакомой вкладкой генератора держите три факта. Хаотичный вид не доказывает непредсказуемость. Math.random может пройти статистические тесты и всё равно не быть CSPRNG. Увидеть getRandomValues в бандле недостаточно: нужен путь, по которому вы кликнули, и канарейка, что пароль не ушёл как рабочие данные. Наблюдения, которые можно повторить, заслуживают места в вашем чек-листе ревью.
Дополнительное чтение
«Локальное шифрование, ничего не загружается» — тоже не доказательство: ищите канарейку в Network в заметке Как проверить, что локальное шифрование в браузере не отправило открытый текст. Локальный список слабых паролей ловит распространённые пароли и не является поиском по всей сети — см. Что доказывает локальный список слабых паролей и почему это не поиск по всей сети. Если позже передадите пароль коллеге один раз, почему часть после # по умолчанию не попадает в HTTP — в Почему после решётки в URL можно класть ключ и когда эта защита ломается.