Слабые пароли и отсутствие KDF

Одной из ключевых проблем при использовании симметричного шифрования в браузере и Node.js становится преобразование пароля пользователя в криптографический ключ. В библиотеке Crypto-js эта задача не решена на уровне архитектурного принуждения: разработчик получает инструменты, но вся ответственность за корректное применение KDF ложится на него.

Почему прямое использование пароля опасно

Пароль пользователя почти никогда не является криптографически стойким материалом. Даже если он кажется сложным, его энтропия существенно ниже требуемой для AES-ключей (128/192/256 бит).

Типичная ошибка выглядит так:

const key = CryptoJS.enc.Utf8.parse(password);
const encrypted = CryptoJS.AES.encrypt("secret data", key);

В этом случае:

  • пароль используется напрямую как ключ
  • отсутствует соль
  • отсутствует итеративное усиление
  • атаки перебора становятся тривиальными при утечке ciphertext

Любой перебор словаря выполняется мгновенно на GPU, поскольку криптографическая стойкость фактически заменяется стойкостью пароля.

Роль KDF и почему он критичен

KDF (Key Derivation Function) предназначена для преобразования слабого входного значения (пароля) в криптографически стойкий ключ.

Её задачи:

  • замедлить перебор (key stretching)
  • добавить уникальность через соль
  • выровнять распределение ключевого пространства
  • защитить от радужных таблиц

В идеале используются современные KDF:

  • Argon2 (устойчив к GPU/ASIC)
  • scrypt (memory-hard)
  • bcrypt (исторически распространён)

Что предлагает Crypto-js

Crypto-js предоставляет базовую реализацию PBKDF2:

const salt = CryptoJS.lib.WordArray.random(128/8);

const key = CryptoJS.PBKDF2(password, salt, {
  keySize: 256/32,
  iterations: 10000
});

Однако важно понимать ограничения:

  • отсутствует Argon2
  • нет memory-hard алгоритмов
  • PBKDF2 не защищает от параллельных GPU атак так эффективно, как современные KDF
  • нет встроенной политики безопасности (разработчик сам выбирает параметры)

Типичная ошибка: отсутствие соли

Одна из наиболее критичных проблем — повторное использование пароля без соли:

const key = CryptoJS.PBKDF2(password, "static_salt", {
  iterations: 1000
});

Последствия:

  • одинаковые пароли → одинаковые ключи
  • возможность предварительного вычисления таблиц
  • компрометация всех пользователей при утечке одного ciphertext

Соль должна быть:

  • случайной
  • уникальной для каждого сообщения/пользователя
  • достаточной длины (≥ 128 бит)

Недостаточные итерации

PBKDF2 увеличивает стойкость за счёт вычислительной нагрузки. Часто встречается опасная практика:

iterations: 1000

Это значение давно считается недостаточным.

Проблема:

  • современные GPU перебирают миллионы ключей в секунду
  • низкие итерации делают защиту номинальной

Реалистичный диапазон:

  • минимум: 100 000+
  • лучше: 300 000 – 600 000 (в зависимости от производительности клиента)

Архитектурная проблема Crypto-js: отсутствие enforced security model

Crypto-js не навязывает безопасный путь использования. Это означает:

  • можно легко обойти KDF
  • можно использовать AES напрямую с паролем
  • нет предупреждений или ограничений
  • нет встроенного безопасного профиля “по умолчанию”

В результате безопасность становится не свойством библиотеки, а дисциплиной разработчика.

Сравнение подходов

Подход Стойкость Проблема
AES(password) низкая нет KDF
PBKDF2(10k) средняя устаревшие параметры
PBKDF2(300k + salt) приемлемая нет memory-hard защиты
Argon2 высокая не поддерживается Crypto-js

Практическая схема безопасного использования

Минимально корректная реализация с Crypto-js:

const salt = CryptoJS.lib.WordArray.random(16);

const key = CryptoJS.PBKDF2(password, salt, {
  keySize: 256 / 32,
  iterations: 300000,
  hasher: CryptoJS.algo.SHA256
});

const encrypted = CryptoJS.AES.encrypt(data, key.toString());

Дополнительно необходимо:

  • хранить salt вместе с ciphertext
  • проверять корректность декодирования
  • избегать повторного использования параметров

Проблема масштабируемости атак

Даже при использовании PBKDF2 атаки становятся эффективными из-за:

  • параллелизации вычислений на GPU
  • предсказуемости пользовательских паролей
  • утечек баз данных хэшей

Если пароль слабый (например, словарный), KDF лишь немного замедляет атаку, но не предотвращает её.

Отсутствие memory-hard защиты

Ключевая слабость Crypto-js в современном контексте — отсутствие алгоритмов, требующих большого объёма памяти.

Argon2 и scrypt делают атаку дорогой не только по CPU, но и по памяти. PBKDF2:

  • требует только CPU
  • легко масштабируется на GPU
  • не ограничивает параллельные атаки

Практическое следствие для веб-приложений

Использование Crypto-js с паролями в браузере приводит к системным рискам:

  • клиентская криптография зависит от слабого источника энтропии (пароль)
  • злоумышленник, получивший ciphertext, может атаковать его офлайн
  • отсутствие современных KDF снижает долговременную стойкость

Особенно критично это для:

  • заметок и vault-приложений
  • клиентского шифрования файлов
  • хранения токенов и секретов в браузере

Рекомендация по архитектуре

Если Crypto-js используется в системе, где есть пароли пользователей, необходимо:

  • всегда использовать PBKDF2 с высокой итерацией
  • генерировать уникальную соль
  • избегать прямого AES(password)
  • при высоких требованиях безопасности выносить KDF на сервер или использовать более современные библиотеки

Слабые пароли и отсутствие устойчивого KDF-подхода превращают криптографическую защиту в иллюзию безопасности, где основным фактором становится не алгоритм, а человеческий выбор пароля.