883 слов
4 минуты

G-PASS TERMINAL

Киберпанк-генератор паролей: разбор под капотом#

Короче, запилил генератор паролей. Но не очередную поделку “возьми Math.random и склей буковки”, а что-то более серьёзное. С криптографией, энтропией и тремя режимами на выбор. Давайте разберём, что там внутри.

Три режима — три философии#

1. Стандартный режим#

Классика жанра: берём пул символов, кидаем кости, получаем Xk#9mP$2nL. Но есть нюансы.

Гарантия категорий. Если выбраны нижний регистр, верхний, цифры и символы — пароль гарантированно будет содержать минимум по одному символу из каждой категории. Никаких “опс, а цифр-то и нет”.

// Сначала берём по одному из каждой категории
let chars: string[] = categories.map((c) => getSecureRandomChar(c.chars));

// Потом добиваем из общего пула
while (chars.length < length) {
    chars.push(getSecureRandomChar(pool));
}

// И перемешиваем Fisher-Yates
for (let i = chars.length - 1; i > 0; i--) {
    const j = secureRandomInt(i + 1);
    [chars[i], chars[j]] = [chars[j], chars[i]];
}

Поддержка кириллицы. Переключаешь тумблер — и вместо 52 латинских букв получаешь 66 кириллических. Пул символов увеличивается, энтропия растёт. А ещё это ломает мозг злоумышленнику, который ожидает латиницу.

2. XKCD-режим#

Помните тот самый комикс? correct horse battery staple — легко запомнить, сложно подобрать. Реализовал именно этот подход.

Словарь — ~180 слов из кибер/техно-тематики:

correct horse battery staple system hacking cyber neural matrix logic 
ghost shell neon laser future data access denied granted protocol...

Размер словаря = 180 слов~7.49 бит на слово.

5 слов = ~37.5 бит только от слов. Добавляем случайную капитализацию (+5 бит) и число 00-99 (+6.6 бит) — получаем ~49 бит. Для онлайн-атак более чем достаточно.

3. Фонетический режим#

Идея: генерировать не случайную кашу и не настоящие слова, а произносимые псевдослова. Bakemi-Toruva-Sinelo-42 — не существует ни в одном языке, но ты можешь это произнести и запомнить.

Четыре паттерна слогов:

  • CV — согласная + гласная (ба, ке, ми) — самый простой
  • CVC — согласная + гласная + согласная (бак, кем, мир)
  • CVCC — согласная + гласная + две согласных (банк, керн)
  • CCVC — две согласных + гласная + согласная (стар, крик)
const generateSyllable = (): string => {
    switch (phoneticPattern) {
        case "cv":
            return getSecureRandomChar(consonants) + getSecureRandomChar(vowels);
        case "cvc":
            return getSecureRandomChar(consonants) + 
                   getSecureRandomChar(vowels) + 
                   getSecureRandomChar(consonants);
        // ...
    }
};

Латинские согласные: 18 штук (bcdfghjklmnprstvwz) Латинские гласные: 5 штук (aeiou)

Энтропия слога CV = log₂(18) + log₂(5) ≈ 4.17 + 2.32 = 6.49 бит

Три слова по три слога = 9 слогов = ~58 бит. Неплохо для чего-то, что можно произнести!

И да, тут тоже есть кириллица. Кириллические согласные/гласные дают чуть больше энтропии за счёт большего алфавита.


Криптографически безопасный RNG#

Math.random() — это PRNG (псевдослучайный генератор). Для паролей не годится. Используем Web Crypto API:

const secureRandomInt = (max: number): number => {
    if (max <= 0) return 0;
    const randomBuffer = new Uint32Array(1);
    const maxValid = Math.floor(0xffffffff / max) * max;

    do {
        crypto.getRandomValues(randomBuffer);
    } while (randomBuffer[0] >= maxValid);

    return randomBuffer[0] % max;
};

Rejection sampling — важная штука. Если max не делит 2³² нацело, простой % max даёт смещение (bias). Например, для max = 3:

  • 0xFFFFFFFF % 3 = 0
  • Числа 0, 1, 2 будут выпадать не с равной вероятностью

Решение: отбрасываем значения ≥ maxValid и генерируем заново. Да, теоретически может зациклиться навечно. Практически — вероятность даже одного retry около 0.0000002%.


Расчёт энтропии#

Энтропия — это не “сколько символов в пароле”, а “сколько бит информации нужно угадать”. Чем больше — тем лучше.

Стандартный режим#

Наивный подход: length × log₂(poolSize). Но это неточно, потому что мы гарантируем символы из каждой категории.

Правильный расчёт:

// 1. Биты от обязательных символов
for (const cat of categories) {
    totalBits += Math.log2(cat.size);
}

// 2. Биты от остальных символов (из общего пула)
if (L > k) {
    totalBits += (L - k) * Math.log2(poolSize);
}

// 3. Биты от перестановок — C(L, k) способов расставить обязательные
if (k > 0 && L > k) {
    totalBits += logBinomial(L, k);
}

Функция logBinomial считает log₂(C(n,k)) без переполнения:

const logBinomial = (n: number, k: number): number => {
    let result = 0;
    for (let i = 0; i < k; i++) {
        result += Math.log2(n - i) - Math.log2(i + 1);
    }
    return result;
};

XKCD и фонетический режимы#

Тут проще:

  • Слова/слоги независимы → умножаем количество на биты каждого
  • Случайная капитализация: +1 бит на слово
  • Число: +log₂(range) бит

Оценка времени взлома#

Беру пессимистичный сценарий: офлайн-атака со скоростью 10¹⁰ хешей/сек (мощная GPU-ферма).

const combinations = Math.pow(2, entropyBits);
const speed = 1e10; // 10 миллиардов в секунду
const seconds = combinations / speed / 2; // В среднем находим за половину перебора

Потом форматирую в человекочитаемый вид:

if (seconds < universe) return `${(seconds / millennium).toFixed(0)} тысячелетий`;
return `${(seconds / universe).toFixed(2)}x возраст Вселенной`;

80 бит энтропии = ~1.9 миллиарда лет при 10¹⁰/сек. Думаю, достаточно.


Мелкие радости#

QR-код — сгенерировал пароль, показал QR, отсканировал телефоном. Удобно для переноса на устройства без синхронизации.

История в localStorage — последние 20 паролей сохраняются локально. Нажал — скопировал. Закрыл вкладку — история осталась.

Визуальная шкала энтропии — от красного (< 20 бит, взлом мгновенный) до белого (256+ бит, AES-256 уровень). С маркерами на 50, 80, 128, 192 бита.


Что можно улучшить?#

  1. Больший словарь для XKCD. 180 слов — это ~7.5 бит. EFF wordlist даёт 7776 слов = ~12.9 бит на слово. Но тогда слова будут менее “тематические”.

  2. Passphrase с грамматикой. The lazy purple robot hacks silently — ещё запоминаемее, но сложнее реализовать.

  3. Проверка на утечки. Интеграция с Have I Been Pwned API (k-anonymity, конечно).

  4. Экспорт в KeePass/Bitwarden. Генерируешь — сразу в менеджер.


Итого#

Генератор получился с тремя режимами под разные задачи:

  • Стандартный — для систем с жёсткими требованиями “минимум 1 цифра, 1 символ…”
  • XKCD — когда нужно запомнить без менеджера
  • Фонетический — компромисс между случайностью и произносимостью
  • Попробовать генератор можно здесь
  • Пощупать исходники можно здесь

P.S. Да, я знаю про 1Password и Bitwarden. Но иногда хочется понимать, как это работает под капотом. И сделать своё.

G-PASS TERMINAL
https://guilliman.ru/posts/password_generator/
Автор
Guilliman
Опубликовано
2025-12-20
Лицензия
CC BY-NC-SA 4.0