Статьи

Использование Halite для конфиденциальности и двустороннего шифрования электронных писем

Криптография — сложный вопрос. На самом деле есть одно золотое правило:

* Не применяйте криптографию самостоятельно *

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

  • Не используйте один и тот же ключ для шифрования всего
  • Не используйте сгенерированный ключ напрямую для шифрования
  • При создании значений, которые вы не хотите угадывать, используйте криптографически безопасный генератор псевдослучайных чисел ( CSPRNG )
  • Зашифруйте, затем MAC (или принцип криптографической гибели )
  • Принцип Керкхоффса: криптосистема должна быть защищенной, даже если все о ней, кроме ключа, является общедоступным

Изображение ключей, висящих на струнах

Некоторые из криптографических терминов, используемых в этой статье, могут быть определены следующим образом:

  • Ключ : фрагмент информации, определяющий функциональный выход криптографического алгоритма.
  • CSPRNG : также известный как детерминированный генератор случайных битов, представляет собой алгоритм для генерации последовательности чисел, свойства которой аппроксимируют свойства последовательностей случайных чисел (или байтов). Чтобы быть криптографически защищенным, PRNG должен:
    • Пройдите статистические тесты на случайность
    • Хорошо держитесь под серьезной атакой, даже когда часть их начального или рабочего состояния становится доступной для атакующего.
  • MAC : короткая информация, используемая для подтверждения того, что сообщение было получено от указанного отправителя (его подлинность) и не было изменено при передаче (его целостность). Он принимает в качестве входных данных секретный ключ и сообщение произвольной длины для аутентификации и выдает MAC.

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

Некоторые библиотеки реализуют криптографические примитивы и операции и оставляют множество решений разработчику. Примерами этого являются собственная крипто библиотека php или php-шифрование Defuse . Некоторые PHP-фреймворки реализуют свои собственные крипто, как Zend Framework Zend- Crypt или Laravel .

Тем не менее, есть одна библиотека, которая отличается от остальных своей простотой и берет на себя большую ответственность разработчика за лучшие практики, помимо использования библиотеки libsodium . В этой статье мы собираемся исследовать Halite .

галит

Предполагается, что у вас уже установлен и работает PHP 7, веб-сервер и сервер MySQL. Возможно, вы захотите взглянуть на Homestead Improved для такой среды. Чтобы использовать Halite, наша система должна иметь библиотеку libsodium, а также расширение PHP libsodium. Они могут быть установлены с помощью следующих команд. Как всегда, в зависимости от конфигурации вашей системы и установленных пакетов, ваш пробег может варьироваться :

Установить в Ubuntu

sudo apt-get install build-essential wget https://github.com/jedisct1/libsodium/releases/download/1.0.10/libsodium-1.0.10.tar.gz tar -xvf libsodium-1.0.10.tar.gz cd libsodium-1.0.10/ sudo ./configure sudo make sudo make install sudo apt-get install php-pear php7.0-dev sudo pecl install libsodium 

Установить в CentOS

 sudo yum groupinstall "Development tools" wget https://github.com/jedisct1/libsodium/releases/download/1.0.10/libsodium-1.0.10.tar.gz tar -xvf libsodium-1.0.10.tar.gz cd libsodium-1.0.10/ sudo ./configure sudo make sudo make install yum install php-pear php-devel sudo pecl install libsodium 

Вам нужно будет отредактировать файл php.ini включив в extension=libsodium.so строку extension=libsodium.so и перезапустите веб-сервер или php-fpm .

Приложение

Чтобы продемонстрировать некоторые основные функции Halite, давайте создадим набор служб RESTful для простого приложения для обмена сообщениями, подобного электронной почте, в котором мы хотим зашифровать сообщения, отправляемые между пользователями. Пожалуйста, примите во внимание, что это будет просто крошечный и простой пример, многие стандартные функции электронной почты будут отсутствовать. Если вы хотите просмотреть полный исходный код, взгляните на репозиторий github .

Таблицы базы данных имеют следующую схему:

Схема базы данных

Наиболее важные поля:

стол поле цель
пользователи поваренная соль содержит случайную двоичную строку для каждого пользователя
Сообщения users_id идентификатор пользователя, которому предназначено сообщение
Сообщения fromUserId идентификатор пользователя, с которого отправлено сообщение

Давайте начнем с инициализации файла composer.json , а затем с помощью Composer объявим зависимость нашего проекта от библиотеки Halite.

 composer init composer require paragonie/halite 

В этом примере я буду использовать silex для маршрутизации запросов в приложение в стиле MVC, phpdotenv для хранения и загрузки переменных среды и доктрину orm для взаимодействия с базой данных с использованием объектов php.

 composer require silex/silex:~2.0 vlucas/phpdotenv doctrine/orm 

После успешной загрузки библиотек файл composer.json необходимо изменить, чтобы он выглядел следующим образом:

 { "name": "yourName/projectName", "description": "Sitepoint tutorial for Halite", "authors": [ { "name": "yourName", "email": "yourEmail" } ], "require": { "silex/silex": "^1.3", "vlucas/phpdotenv": "^2.2", "paragonie/halite": "^2.1", "doctrine/orm": "^2.5" }, "autoload": { "psr-4": {"Acme\\": "src/"} } } 

Пространство имен Acme будет содержать наши пользовательские классы.

Сервисы RESTful в нашем примере приложения имеют следующую структуру:
RESTful услуги

метод URL описание
ПОЛУЧИТЬ / пользователей Получить список пользователей
ПОЛУЧИТЬ / пользователей / {} идентификатор пользователя Получить информацию о данном пользователе
ПОЛУЧИТЬ / пользователей / {USERID} / сообщения Получить список сообщений, отправленных данному пользователю
ПОЧТА / пользователей / {USERID} / сообщения Отправить сообщение пользователю
ПОЛУЧИТЬ / пользователей / {идентификатор} / сообщения / {MessageId} Получить сообщение от данного пользователя

Для этого приложения мы хотим зашифровать тему и сообщение, поэтому URL-адреса, выделенные жирным шрифтом, являются теми, которые используют функции Halite.

В этой статье мы будем использовать симметричное шифрование. То есть мы используем один и тот же ключ для шифрования и дешифрования сообщения.

Учитывая, что в системе есть следующие пользователи (путем вызова службы списка пользователей):

 #Request GET /users HTTP/1.1 Host: yourhost.dev Cache-Control: no-cache #Response [ { "id": 1, "name": "John Smith" }, { "id": 2, "name": "Jane Doe" } ] 

Когда мы хотим отправить сообщение Джону от Джейн, мы должны отправить следующий запрос:

 POST /users/1/messages HTTP/1.1 Host: yourhost.dev Cache-Control: no-cache { "from": "2", "subject": "This is my subject", "message": "super secret information!!!" } 

Пример проекта использует Silex и Doctrine для выполнения некоторых тяжелых работ, однако в этой статье не рассматривается, как они работают. Для этого, пожалуйста, смотрите это введение в Silex и некоторые из наших постов о Доктрине .

Грубо говоря, Silex будет пересылать HTTP-запросы на контроллер. Контроллер вызовет службу, где все станет интересным. Давайте посмотрим на код из метода \Acme\Service\Message::save .

 <?php namespace Acme\Service; use Acme\Model\Message as MessageModel; use Acme\Model\Repository\Message as MessageRepository; use Acme\Model\User as UserModel; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use ParagonIE\Halite\KeyFactory; use ParagonIE\Halite\Symmetric\AuthenticationKey; use ParagonIE\Halite\Symmetric\Crypto; use ParagonIE\Halite\Symmetric\EncryptionKey; /** * Class Message * @package Acme\Service */ class Message { // ... SOME CODE HERE ... /** * @param $from * @param $to * @param $subject * @param $message * @return string * @throws \ParagonIE\Halite\Alerts\InvalidSalt */ public function save($from, $to, $subject, $message) { /* * create a new model object to hold the message */ $messageModel = new MessageModel; /** @var EntityRepository $userRepository */ $userRepository = $this->em->getRepository('Acme\Model\User'); /* * search for the sender and recipient users */ /** @var UserModel $fromUserModel */ $fromUserModel = $userRepository->find($from); /** @var UserModel $toUserModel */ $toUserModel = $userRepository->find($to); if (!$fromUserModel || !$toUserModel) { throw new \InvalidArgumentException('From or to user does not exist'); } /* * create a placeholder for data, in order to generate a message id, used later to encrypt data. */ $messageModel->setFromUser($fromUserModel)->setToUser($toUserModel); $this->em->persist($messageModel); $this->em->flush(); /* * Retrieve the salts for both the sender and the recipient */ $toUserSalt = $toUserModel->getSalt(); /* * create encryption keys concatenating user's salt, a string representing the target field to be * encrypted, the message unique id, and a system wide salt. */ $encryptionKeySubject = KeyFactory::deriveEncryptionKey( base64_decode($toUserSalt) . 'subject' . $messageModel->getId(), base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT'))); $encryptionKeyMessage = KeyFactory::deriveEncryptionKey( base64_decode($toUserSalt) . 'message' . $messageModel->getId(), base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT'))); /* * encrypt the subject and the message, each with their own encryption key */ $cipherSubject = Crypto::encrypt($subject, $encryptionKeySubject, true); $cipherMessage = Crypto::encrypt($message, $encryptionKeyMessage, true); $messageModel->setSubject(base64_encode($cipherSubject))->setMessage(base64_encode($cipherMessage)); $this->em->persist($messageModel); $this->em->flush(); return $messageModel->getId(); } } 

Сначала создается объект, представляющий запись из таблицы сообщений, которая будет использоваться с Doctrine. Это облегчит запись данных в базу данных. Этот объект имеет атрибуты, которые отображаются 1: 1 в таблице сообщений.

Затем создается хранилище Doctrine для таблицы пользователей. Хранилище используется для простого извлечения данных в объекты модели. Простота извлечения данных показана с помощью операторов $userRepository->find($from) и $userRepository->find($to) без написания одной команды SQL.

Полученные модели затем присваиваются нашей модели сообщений, а затем сохраняются в базе данных с помощью диспетчера сущностей Doctrine через $this->em->persist($messageModel); и $this->em->flush(); заявления. Это делается для того, чтобы первичный ключ, созданный для этой записи, можно было извлечь и использовать позже для шифрования данных. Для получения дополнительной информации о двойственной природе постоянства / сброса см. Этот пост .

Каждый пользователь в нашей базе данных содержит уникальную строку случайных байтов, называемую «соль». Эта соль, которая закодирована в base64 для хранения в базе данных в виде серии печатных символов, выглядит следующим образом:

данные таблицы пользователей

Нам нужно получить соль отправителя и получателя, чтобы использовать ее для шифрования сообщения. Как вы, возможно, уже догадались, это легко сделать с помощью моделей, загруженных выше с помощью $toUserSalt = $toUserModel->getSalt(); и $fromUserSalt = $fromUserModel->getSalt(); ,

Наконец начинается настоящее веселье. Настало время использовать библиотеку Halite. Одно из правил криптографии предписывает никогда не использовать предварительно сгенерированный ключ для шифрования данных, а также не использовать один и тот же ключ несколько раз. Мы будем использовать \ParagonIE\Halite\KeyFactory для генерации ключей шифрования. Пожалуйста, внимательно посмотрите на следующее утверждение:

 $encryptionKeySubject = KeyFactory::deriveEncryptionKey(base64_decode($toUserSalt) . 'subject' . $messageModel->getId(), base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT'))); 

Метод \ParagonIE\Halite\KeyFactory::deriveEncryptionKey получает 2 параметра: пароль для получения и соль. Обратите внимание, что мы используем конкатенацию соли получателя (которая сначала должна быть декодирована с помощью base64), строки ‘subject’ и уникального идентификатора сообщения в качестве параметра пароля; это обеспечит уникальность «пароля» для этого получателя / этой записи / этого поля. Второй параметр будет содержать набор соли приложения как переменную среды благодаря библиотеке dotenv. Эта соль хранится в файле .env (вместе с учетными данными базы данных), закодированными в base64. Файл .env содержит соль следующим образом:

 HALITE_ENCRYPTION_KEY_SALT="/t4xwLjjkG0RoRHGkmOQVw==" 

Эта соль представляет собой 16-байтовую (согласно требованиям алгоритма argon2 , используемого в функции вывода ключа Halite) случайную двоичную строку. Эта строка может быть сгенерирована с помощью base64_encode(random_bytes(16)) . Чтобы этот подход был безопасным, сервер приложений и сервер базы данных должны быть установлены на отдельном оборудовании.

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

Теперь фактическое шифрование может иметь место

 $cipherSubject = Crypto::encrypt($subject, $encryptionKeySubject, true); $cipherMessage = Crypto::encrypt($message, $encryptionKeyMessage, true); 

Первый параметр \ParagonIE\Halite\Symmetric\Crypto::encrypt — это простой текст, второй параметр — ключ шифрования, полученный выше. Третий параметр скажет Halite вернуть необработанную двоичную строку. Поведение по умолчанию — возвращать шестнадцатеричное представление. Это, конечно, зависит от ваших предпочтений.

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

Теперь мы готовы присвоить зашифрованный текст нашей модели (сначала $messageModel->setSubject(base64_encode($cipherSubject))->setMessage(base64_encode($cipherMessage)); base64) $messageModel->setSubject(base64_encode($cipherSubject))->setMessage(base64_encode($cipherMessage)); чтобы иметь возможность сохранить его в нашей базе данных.

Посмотрим полный запрос и ответ

Запрос

 POST /users/1/messages HTTP/1.1 Host: yourhost.dev Cache-Control: no-cache { "from": "2", "subject": "This is my subject", "message": "super secret information!!!" } 

отклик

 { "messageId": 1 } 

Если мы запросим базу данных напрямую, запись будет выглядеть следующим образом:

 *************************** 1. row *************************** id: 1 users_id: 1 fromUserId: 2 subject: MUICAV13/brmlurlUIF6TOmhD6duztBI7vYzd4PGrJu8TinhAm69Kzv5QVFpNO55mssxsthPqmwb7l/py1iTCl0tSHUcB5Wsep0bcYDztIPhZ3g7VOcKKqu+YBTcuWprMvM22nvVdzcismGvCjkVW5hqCuNaCJPaUY+7VKRqCLg6FPY4WLMOdbY2yotQ4Q== message: MUICAaSGjcjwaDUORzctiK8rDla+w2Wm/6Z75EP7LJsRZbCk5zVHC/R6oxaJ6VrVbaB6WG1k9xtUcCt9fGN40r3zjFxp4M24QEdVu18t7A8N4wbiwd1W7LqbqjieziBchtKIJjE4oM68BlKxJMGO020GSnwNBuXIaz1b1MRVQDEsm1eT/oTx1oeLvwG1X94raKpHmK7D 

Если по какой-либо причине мы должны были отправить точно такой же запрос:

 POST /users/1/messages HTTP/1.1 Host: yourhost.dev Cache-Control: no-cache { "from": "2", "subject": "This is my subject", "message": "super secret information!!!" } 
 { "messageId": 2 } 

И снова запросите базу данных:

 *************************** 1. row *************************** id: 1 users_id: 1 fromUserId: 2 subject: MUICAV13/brmlurlUIF6TOmhD6duztBI7vYzd4PGrJu8TinhAm69Kzv5QVFpNO55mssxsthPqmwb7l/py1iTCl0tSHUcB5Wsep0bcYDztIPhZ3g7VOcKKqu+YBTcuWprMvM22nvVdzcismGvCjkVW5hqCuNaCJPaUY+7VKRqCLg6FPY4WLMOdbY2yotQ4Q== message: MUICAaSGjcjwaDUORzctiK8rDla+w2Wm/6Z75EP7LJsRZbCk5zVHC/R6oxaJ6VrVbaB6WG1k9xtUcCt9fGN40r3zjFxp4M24QEdVu18t7A8N4wbiwd1W7LqbqjieziBchtKIJjE4oM68BlKxJMGO020GSnwNBuXIaz1b1MRVQDEsm1eT/oTx1oeLvwG1X94raKpHmK7D *************************** 2. row *************************** id: 2 users_id: 1 fromUserId: 2 subject: MUICAeF/ci3fG7YWIhuLCU0pIxpbNDQ10KVWjSZF4G3K8lNVdV81LeveFyhAt/9PvJQy1ePLzl3EupJCROEQ+/L4nHUFRvCekEter2AQvnlyW03cxfjzZ6XwXBUkhzhZrT22JzxL7gSzpC6fPInAzsdDRIqhK/50wPhg/Wr29pH+PT/qwfuKr0rQWdyIfg== message: MUICAYOTzGO7DcpX7bVn4BMH4DKW+JCH1Eacj+lPx9I6eOHjNTA9jHT+u+VEz7cfdiitySU5UYYpBz+IBpV7b8hp/hwkqoLP7Durq/sPRJE/qMY9naKumaPXYW6XMhkysfwnwAIWQhjuiu/r9ARBwdTP3AgFNfVVc/jTlCuaPPZ8jw39xKCO5eNU3UyBrcXFhsGBnS6B 2 rows in set (0.00 sec) 

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

Теперь время расшифровки!

 <?php namespace Acme\Service; use Acme\Model\Message as MessageModel; use Acme\Model\Repository\Message as MessageRepository; use Acme\Model\User as UserModel; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use ParagonIE\Halite\KeyFactory; use ParagonIE\Halite\Symmetric\AuthenticationKey; use ParagonIE\Halite\Symmetric\Crypto; use ParagonIE\Halite\Symmetric\EncryptionKey; /** * Class Message * @package Acme\Service */ class Message { // ... SOME CODE HERE ... /** * @param $userId * @param $messageId * @return array * @throws \InvalidArgumentException */ public function get($userId, $messageId) { /** @var MessageRepository $repository */ $repository = $this->em->getRepository('Acme\Model\Message'); /** @var MessageModel $message */ if (($message = $repository->find($messageId)) == true) { $toUser = $message->getToUser(); /* * Verify that the message belongs to the intended user */ if ($toUser->getId() == $userId) { $toUserSalt = $toUser->getSalt(); $fromUser = $message->getFromUser(); $encryptionKeySubject = KeyFactory::deriveEncryptionKey(base64_decode($toUserSalt) . 'subject' . $message->getId(), base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT'))); $encryptionKeyMessage = KeyFactory::deriveEncryptionKey(base64_decode($toUserSalt) . 'message' . $message->getId(), base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT'))); $plainSubject = Crypto::decrypt(base64_decode($message->getSubject()), $encryptionKeySubject, true); $plainMessage = Crypto::decrypt(base64_decode($message->getMessage()), $encryptionKeyMessage, true); return [ 'id' => $message->getId(), 'subject' => $plainSubject, 'message' => $plainMessage, 'name' => $fromUser->getName(), ]; } } throw new \InvalidArgumentException('Message not found'); } } 

Мы получаем хранилище таблицы сообщений с $repository = $this->em->getRepository('Acme\Model\Message'); и продолжайте загружать сообщение с $message = $repository->find($messageId) . Чтобы расшифровать, мы должны снова получить ключ шифрования так же, как мы это делали при первом сохранении сообщения, только на этот раз мы будем использовать метод \ParagonIE\Halite\Symmetric\Crypto::decrypt :

 $plainSubject = Crypto::decrypt(base64_decode($message->getSubject()), $encryptionKeySubject, true); $plainMessage = Crypto::decrypt(base64_decode($message->getMessage()), $encryptionKeyMessage, true); 

Этот метод получает 3 параметра: текст шифра, ключ шифрования и флаг, чтобы сообщить методу, что он должен ожидать необработанные двоичные байты в тексте шифра. (помните, что мы сохранили его в кодировке base64 в базе данных, поэтому нам нужно его декодировать в base64) . Теперь мы можем выполнить запрос на получение сообщения по идентификатору:

Запрос

 GET /users/1/messages/1 HTTP/1.1 Host: yourhost.dev Cache-Control: no-cache 

отклик

 { "id": 1, "subject": "This is my subject", "message": "super secret information!!!", "name": "Jane Doe" } 

Вы можете посмотреть полный исходный код здесь .

Вывод

Шифрование и дешифрование очень просто с Halite. Это не означает, что приведенный выше пример является суперзащищенным. Есть еще много возможностей для улучшения, и у Halite есть и другие интересные особенности, которые могут быть продемонстрированы в следующей статье. Помните, что вы не должны воспринимать приведенную выше реализацию как готовую продукцию — она ​​предназначена только для образовательных целей

Как вы делаете шифрование / дешифрование в PHP? Вы использовали Halite? Дайте нам знать!