С распространением Service Worker и PWA все больше проектов будут иметь зашифрованное хранилище на стороне клиента в качестве требования. В этом посте мы рассмотрим широко используемую криптографическую библиотеку JS, проведем базовую комплексную проверку, а затем покажем путь к замене ее собственным API веб-криптографии браузера.

Базовая комплексная проверка

CryptoJS — популярная библиотека для криптографии в браузере. Проведя некоторые базовые исследования, я убедился, что это не криптографическая библиотека промышленного качества. Отнюдь не.

То, что я нашел, — это библиотека квази-неизвестного происхождения, поддерживаемая любителем, небезопасные параметры по умолчанию, отсутствие программы поощрения за ошибки, никакого опубликованного аудита любого рода и никакого заметного источника дохода. Это как лучшие хиты сломанных криптографических библиотек.

Тот факт, что его загружают около 1 миллиона раз в неделю, красноречиво говорит об опытности среднего пользователя npm и отсутствии инженерных стандартов в сообществе фронтенда в целом.

Но есть исключения. Я более чем рад представить вам эту жемчужину: Встреча ответственного разработчика с CryptoJS в двух действиях.

Путь вперед

В этом посте мы заменим модуль AES CryptoJS на API веб-криптографии. Самая большая проблема при поэтапном отказе от CryptoJS связана с данными, ранее зашифрованными им. Мы можем оставить зависимость для этой цели, но лично я бы предпочел немедленно удалить ее и вместо этого использовать стандартные API для расшифровки старых данных.

CryptoJS использует стандартный алгоритм AES-CBC, который также входит в состав API веб-криптографии. Web Crypto включает только одну схему заполнения для полезной нагрузки, отличной от блока, но она же используется CryptoJS по умолчанию.

Однако это становится более сложным в отношении вывода ключей. Пик под капотом показывает, что алгоритм получения ключа из фразы-пароля не стандартизирован и, следовательно, не является частью Web Crypto, и он использует сломанный хэш MD5, который также не является частью Web Crypto. К сожалению, нам придется поддерживать это, чтобы расшифровать старые данные, но мы можем принять некоторые меры, чтобы предотвратить злоупотребление ими.

Обзор

Наша функция высокого уровня будет выглядеть так:

Пара замечаний:

  • Параметры по умолчанию взяты из CryptoJS.
  • Я предполагаю, что открытый текст представляет собой строку. Если нет, вам придется удалить часть декодирования текста.
  • Мы принимаем шифр в base64, потому что это значение по умолчанию, возвращаемое функцией CryptoJS toString. Вам придется немного изменить код, чтобы он принимал другие форматы.
  • Я использую подробные имена переменных, потому что в противном случае очень легко перепутать типы и кодировки.
  • Все размеры в этой статье кратны 32-битным. Я выбрал этот модуль, потому что он также используется CryptoJS, что упрощает быстрому npm-хакеру копирование и вставку кода и его работу. Чтобы сообщить об этом, по общему признанию, странном выборе всем остальным, я позаимствовал обозначение DWORD у Microsoft, где обычно известно, что оно 32-битное.
  • Я следую проверенной практике React — давать опасным функциям многословные и неудобные имена.
+----------+----------+------------------------------------------
| Salted__ |  <salt>  | <ciphertext...
+----------+----------+------------------------------------------

Далее мы рассмотрим разбор входного зашифрованного текста. CryptoJS добавляет к зашифрованному тексту префикс Salted__ (ровно 64 бита), за которым следует 64-битная соль.

Чтобы получить соль и зашифрованный текст как Uint8Array s, мы используем следующее. Я должен отметить, что следующий код в основном извлечен из CryptoJS и переписан с использованием современных идиом JS и API, в частности, типизированных массивов.

Я приведу вспомогательные функции в конце, но достаточно сказать, что они делают именно то, о чем говорят (и работают только в течение Uint8Array с).

Обратите внимание на умножение на 4 для перехода от DWORD к байтам и смещение 4 при получении второго значения Int32 по той же причине.

Затем мы переключаем наше внимание на загадочную функцию dangerouslyDeriveParameters. Здесь мы берем криптографически слабую парольную фразу и превращаем ее в предположительно сильный криптографический ключ. Учитывая параметры по умолчанию, на самом деле это не так.

  • Обратите внимание, что и ключ, и IV получаются из пароля. Я думаю, это было бы нормально, если бы KDF не был таким слабым, а соль случайной и уникальной, но это не то, что функция deriveKey API веб-криптографии поддерживала бы, если бы она реализовывала EVPKDF.
  • Мы разрешаем использовать ключ только для расшифровки. Это делается для предотвращения случайного или преднамеренного использования шифрования, для которого оно слишком слабое.

Далее мы переходим к функции dangerousEVPKDF. Это та же самая функция получения ключа, которая используется OpenSSL (ключевое слово: 'EVP_BytesToKey') и не опасна сама по себе, но в случае жесткого кодирования MD5 в качестве ее хеш-функции это, безусловно, опасно.

Это единственная часть, где мы полагаемся на внешнюю зависимость, поскольку MD5 не предоставляется API веб-криптографии. Я использую js-md5 здесь, потому что он поддерживает буферы массивов, но не проводил никаких исследований по этому поводу. Я подумал, что в любом случае нет такой вещи, как безопасная реализация MD5. Вы были предупреждены.

Обратите внимание, что эту функцию можно было бы сделать гораздо более эффективной с точки зрения времени и пространства за счет предварительного выделения и повторного использования типизированных массивов. Тем не менее, так было легче сделать это правильно, и за ним также должно быть легче следовать.

использование

Вот и все. Теперь мы можем расшифровывать данные, ранее зашифрованные с помощью CryptoJS, с меньшим размером кода, в основном асинхронно и в основном с использованием быстрого собственного кода.

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

Во второй части мы займемся получением более надежного ключа из той же парольной фразы и повторным шифрованием данных. В основном это заурядный код веб-криптографии, и примеры легко доступны в Интернете. Тем не менее, есть некоторая особенность, связанная с работой с префиксом Salted__ и его хорошей интеграцией со структурой, которую мы установили в этом посте.

Первоначально опубликовано на https://qwtel.com 12 августа 2019 г.