С распространением 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 г.