Зашифрованное взаимодействие между клиентом и сервером на Laravel 4

Введение


Всем доброго времени суток! В своей первой статье по Laravel я хочу поделиться своим опытом организации зашифрованного взаимодействия между клиентом (десктопным приложением) и сервером, работающем под управлением Laravel.

В чем суть задачи? Есть приложение, которое работает по принципу подписки: оплатил на сайте скажем месяц использования и месяц пользуешься. Как только этот срок прошел, приложение должно перестать работать. Так как по условию для работы этого приложения требуется подключение к интернету, то самым оптимальным вариантом проверять лицензионность приложения будет опрашивать сервер лицензий, который по совместительству может быть еще и сайтом, через который собственно и происходит оплата и продление периода использования приложения.

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

В этой статье я расскажу о том, как организовать зашифрованное взаимодействие между клиентом и сервером. В качестве серверной площадки будет приложение на движке Laravel 4.1 (последняя версия на момент публикации). Предполагается, что в качестве клиента будет выступать приложение, написанное на C#, но в этой статье я не буду описывать написание клиента. Вместо этого могу порекомендовать статью с CodeProject'а, в которой приводится пример использования криптографии на C#: Encrypting Communication between C# and PHP. Собственно эта статья и стала отправной точкой для моих изысканий.

Я постараюсь осветить следующие вопросы:
  • основы криптографии (речь именно об основах, так как криптографией я не сильно владею);
  • программирование под Laravel, придерживаясь SOLID-дизайна (в идеале, но на практике, для упрощения статьи, не все SOLID-принципы будут соблюдены)
  • Возможно что-то еще

Основы криптографии


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

В асимметричных алгоритмах используются два ключа: открытый и закрытый. Шифрование производится с использованием открытого ключа, при том расшифровка этих данных может быть осуществлена только с использованием закрытого ключа. Помимо шифрования, есть еще такое понятие, как цифровая подпись данных: хэш данных, сформированных с использованием закрытого ключа. Такой хэш проверяется при помощи открытого ключа. Что это значит и для чего это нужно? Это нужно для того, чтобы удостовериться, что полученные данные пришли именно от того, от кого мы ожидаем, и не были никем модифицированы на пути к нам. Асимметричные алгоритмы работают гораздо медленнее симметричных и предназначены больше для шифрования небольшого объема данных, но зато надежнее. В силу этого, как правило, используется следующая схема взаимодействия между клиентом и сервером:
  1. Вначале клиент генерирует случайный ключ для симметричного шифрования.
  2. Далее клиент зашифровывает этот ключ посредством асимметричного алгоритма, используя известный ему заранее публичный ключ сервера.
  3. Зашифрованный ключ отправляется серверу.
  4. Сервер получает и расшифровывает полученный ключ и ответом сообщает клиенту, что все хорошо.
  5. Дальнейший трафик между сервером и клиентом шифруется посредством выбранного симметричного алгоритма.

Для чего это нужно? Для того, чтобы предотвратить атаку типа man in the middle (человек посередине), когда трафик перехватывается и используется злоумышленником в своих целях.

В нашем же случае помимо шифрации данные будут еще подписываться, чтобы клиент был на 100% уверен, что данные получил именно от своего сервера, а не от потенциально подделанного.

Немного о структуре серверного приложения


Я не буду здесь рассказывать, как создать пустое Laravel приложение, об этом можно прочитать на официальном сайте фреймворка. Предположим Вы уже создали приложение с именем MySecureApp. Весь наш код будет расположен внутри директории app. А точнее, помимо контроллеров, моделей и вьюх, создадим вот еще что:
  1. Внутри папки app создайте папку lib, в ней MySecureApp — здесь будут располагаться все наши классы, реализующие бизнес логику приложения
  2. Внутри папки app создайте папку keys. В ней будет храниться наш закрытый ключ.
  3. Отредактируйте файл composer.json в корне приложения, добавив следующие строки:
    "autoload": {
      "classmap": [
        "app/commands",
        "app/controllers",
        "app/models",
        "app/database/migrations",
        "app/database/seeds",
        "app/tests/TestCase.php"
      ],
      "psr-0": {
        "MySecureApp": "app/lib"
      }
    },

    После чего необходимо выполнить команду, чтобы авто-загрузчик видел наши классы:
    composer dump-autoload

  4. У самого нашего приложения должна быть следующая структура директорий:
    /app
      /lib
        /MySecureApp
          /Cryptography - классы, непосредственно осуществляющие шифрование
          /Dto          - Data Transfer Objects
            /Responses  - классы-ответы нашего API
          /Facades      - Фасады для удобного доступа к некоторым классам
          /Filters      - фильтры
          /Helpers      - классы-помощники
          /Providers    - Service providers, регистрирующие наш функционал в приложении Laravel

    Постепенно мы наполним эти директории классами.


 Клиент-серверное взаимодействие


Все взаимодействие между клиентским приложением и сервером будет происходить через единственный контроллер — ApiController. То есть через Url'ы вида mysecureapp/api*

Логика следующая: клиент отправляет POST запрос на интересующий его метод api, передавая каждый параметр в зашифрованном виде. В ответ сервер возвращает JSON-ответ вида:
{
    "data": "<AES encrypted JSON object>",
    "sign": "<RSA signature of data>"
}


Реализация криптографии


В качестве симметричного алгоритма шифрования будем использовать AES. В качестве асимметричного — RSA. К счастью, вместе с Laravel'ом уже поставляется криптографическая библиотека phpseclib, которая содержит все нам необходимое. Начнем с RSA.

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

Для этого нам понадобится установленный на компьютере OpenSSL. Насколько мне известно, на Linux системах он установлен по умолчанию. Для Windows его можно скачать отсюда: http://slproweb.com/products/Win32OpenSSL.html. Лично у меня возникли трудности с использованием Light дистрибутивов — в них не было необходимого для работы openssl.cfg. Поэтому желательно скачать и установить полную версию (~19 Мб). После установки необходимо создать переменную окружения OPENSSL_CONF, указывающую на вышеупомянутый конфиг. Сделать это можно в консоли, набрав
set OPENSSL_CONF = \путь\к\openssl.cfg

Приступим к созданию ключей. Запустите командную строку и перейдите (cd) в директорию, куда только что установили openssl, а точнее во вложенную директорию bin. Для генерации закрытого ключа, выполните последовательно следующие две команды:
openssl genrsa -aes256 -out temp.key 1024
openssl rsa -in temp.key -out private.key

Теперь на основе полученного ключа сгенерируем X509-сертификат, или иными словами — открытый ключ:
openssl req -new -x509 -nodes -sha1 -key private.key -out public.crt -days 365000

Вам будут заданы несколько вопросов, отвечать на которые не обязательно. Можно ответить что угодно.
Итого имеем:
  1. private.key — закрытый ключ
  2. public.crt — открытый

Перенесите их в подготовленную уже для этого папку app/keys, а в конфиг приложения (app/config/app.php) добавьте следующую строчку:
'privateKey' => 'private.key',

Прежде чем приступать к реализации RSA, создадим вспомогательный класс для кодирования/раскодирования строк в/из Base64. Создайте файл app/lib/MySecureApp/Helpers/Base64.php со следующим содержимым:
<?php namespace MySecureApp\Helpers;

class Base64 {
    public static function UrlDecode($x)
    {
        return base64_decode(str_replace(array('_','-'), array('/','+'), $x));
    }

    public static function UrlEncode($x)
    {
        return str_replace(array('/','+'), array('_','-'), base64_encode($x));
    }
}

Ну а теперь приступим к непосредственно реализации RSA. Для этого создадим класс Cryptography в app/lib/MySecureApp/Cryptography/Cryptography.php:
<?php namespace MySecureApp\Cryptography;

use MySecureApp\Helpers\Base64;

class Cryptography {
    /**
     * RSA instance
     * @var \Crypt_RSA
     */
    protected $rsa;

    /**
     * RSA private key
     * @var string
     */
    protected $rsaPrivateKey;

    /**
     * Whether RSA instance is initialized
     * @var bool
     */
    private $isRsaInitialized = false;

    /**
     * Initializes the RSA instance using either provided private key file or config value
     * @param String $privateKeyFile Path to private key file
     * @throws Exception
     */
    public function initRsa($privateKeyFile = '') {
        //
    }

    /**
     * Decrypts RSA-encrypted data
     * @param String $data Data to decrypt
     * @return String
     */
    public function rsaDecrypt($data) {
        //
    }

    /**
     * Encrypts data using RSA
     * @param String $data Data to encrypt
     * @return String
     */
    public function rsaEncrypt($data) {
        //
    }

    /**
     * Signs provided data
     * @param String $data Data to sign
     * @throws \Exception
     * @return string Signed data
     */
    public function rsaSign($data) {
        //
    }
}

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

Приступим к наполнению RSA методов. Начнем с rsaInit(). Алгоритм простой: считываем закрытый ключ, переданный нам в параметре, либо взятый из конфига, и инициализируем класс Crypt_RSA, поставляемый библиотекой phpseclib:
    public function initRsa($privateKeyFile = '') {
        // Если не указан параметр, берем ключ из конфига
        if (!$privateKeyFile) {
            $privateKeyFile = app_path() . '/keys/' . \Config::get('app.privateKey');
        }

        // Проверяем, существует ли такой файл
        if (!\File::exists($privateKeyFile)) {
            Log::error("Error reading private key file.");
            throw new Exception("Error reading private key file.");
        }

        $this->rsaPrivateKey = \File::get($privateKeyFile);

        // Сама инициализация RSA
        $rsa = new \Crypt_RSA();
        $rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1);
        $rsa->loadKey($this->rsaPrivateKey);

        // Запоминаем экземпляр в приватной переменной и 
        // устанавливаем флаг в true во избежание лишних
        // (повторных) инициализаций
        $this->rsa = $rsa;
        $this->isRsaInitialized = true;
    }

Теперь реализуем сами методы для работы сданными:
    public function rsaDecrypt($data) {
        // инициализируем RSA если это еще не было сделано
        if (!$this->isRsaInitialized) {
            $this->initRsa();
        }

        // сама дешифрация
        return $this->rsa->decrypt(Base64::UrlDecode($data));
    }

    // ...

    public function rsaEncrypt($data) {
        // аналогично rsaDecrypt
        if (!$this->isRsaInitialized) {
            $this->initRsa();
        }

        return Base64::UrlEncode($this->rsa->encrypt($data));
    }

    // ...

    public function rsaSign($data) {
        if (!$this->isRsaInitialized) {
            $this->initRsa();
        }

        // проверяем, установлено ли PHP-расширение openssl
        if (!function_exists('openssl_sign')) {
            throw new \Exception("OpenSSL is not enabled.");
        }

        // формируем подпись
        $signature = '';
        $keyId = openssl_get_privatekey($this->rsaPrivateKey);
        openssl_sign($data, $signature, $keyId);
        openssl_free_key($keyId);

        return $signature;
    }

Обратите внимание, что метод rsaDecrypt ожидает, что переданные данные закодированы в Base64. Симметрично, rsaEncrypt возвращает зашифрованные данные, закодированные в Base64.

На этом RSA часть класса Cryptography завершена. Приступим к AES.

Добавляем поля к классу:
    /**
     * AES instance
     * @var \Crypt_AES
     */
    protected $aes;

    /**
     * Whether AES instance is initialized
     * @var bool
     */
    private $isAesInitialized = false;

Теперь методы:
    /**
     * Initializes AES instance using either provided $options or session values
     * @param array $options Array of options, containing 'key' and 'iv' values
     * @throws Exception
     */
    public function initAes($options = array()) {
        // ...
    }

    /**
     * Encrypts data using AES
     * @param String $data Data to encrypt
     * @return String
     */
    public function aesEncrypt($data) {
        // ...
    }

    /**
     * Decrypts AES encrypted data
     * @param String $data Data to decrypt
     * @return String
     */
    public function aesDecrypt($data) {
        // ...
    }

Инициализация AES:
    public function initAes($options = array()) {
        // Если $options пустой, то берем ключ из сессии
        if (empty($options) && Session::has('aes_key') && Session::has('aes_iv')) {
            $options = array(
                'key'   => Session::get('aes_key'),
                'iv'    => Session::get('aes_iv'),
            );
        }

        // Если и в сессии ключа не оказалось, то выбрасываем исключение
        if (!(isset($options['key']) && isset($options['iv']))) {
            Log::error("Either key or iv not set");
            throw new Exception("Either key or iv not set");
        }

        // Запоминаем ключ в сессии
        Session::put('aes_key', $options['key']);
        Session::put('aes_iv', $options['iv']);

        // Инициализируем Crypt_AES, поставляемый библиотекой phpseclib
        $aes = new \Crypt_AES(CRYPT_AES_MODE_CBC);
        $aes->setKeyLength(256);
        $aes->setKey(Base64::UrlDecode($options['key']));
        $aes->setIV(Base64::UrlDecode($options['iv']));
        $aes->enablePadding();

        // Запоминаем и устанавливаем флаг
        $this->aes = $aes;
        $this->isAesInitialized = true;
    }

Теперь сами методы обработки данных:
    public function aesEncrypt($data) {
        // Все по аналогии с RSA
        if (!$this->isAesInitialized) {
            $this->initAes();
        }

        return $this->aes->encrypt($data);
    }

    public function aesDecrypt($data) {
        if (!$this->isAesInitialized) {
            $this->initAes();
        }

        return $this->aes->decrypt($data);
    }

На этом класс Cryptography готов. Но пока это лишь инструмент, а как им пользоваться, хоть вроде и очевидно, будет показано дальше.

Дальше нам нужен инструмент, который бы выдавал расшифрованные входящие данные. Что я имею ввиду? К примеру, клиент хочет авторизоваться, и для этого он (я немного забегаю вперед), отправляет POST запрос на mysecureapp/api/login со следующими данными: email=asdpofih345kjafg и password=zxcvzxcvzxcvzxcv — это зашифрованные AES'ом данные. Для получения расшифрованных данных нам понадобится класс, аналогичный фасаду Input, но возвращающий уже расшифрованные данные. Назовем его DecryptedInput и создадим его в app/lib/MySecureApp/Cryptography/DecryptedInput.php. Реализуем в нем наиболее популярные методы Input'а: get(), all() и only():
<?php namespace MySecureApp\Cryptography;

use MySecureApp\Helpers\Base64;

/**
 * Provides funcitonality for getting decrypted Input paramters
 * (encrypted with AES)
 * Class DecryptedInput
 * @package MySecureApp\Cryptography
 */
class DecryptedInput {

    /**
     * Array of raw (non-decrypted) input parameters
     * @var array
     */
    protected $params;

    /**
     * Array of decrypted values
     * @var array
     */
    protected $decryptedParams = array();

    /**
     * @var Cryptography
     */
    protected $crypt;

    /**
     * @param Cryptography $crypt Injected Cryptography object used for decrypting
     */
    public function __construct(Cryptography $crypt) {
        // Получаем в конструкторе экземпляр Cryptography
        // инъекцией
        $this->crypt = $crypt;

        // Запоминаем все данные в $params
        $this->params = \Input::all();
    }

    /**
     * Returns decrypted input parameter
     * @param $key
     * @return String
     */
    public function get($key) {
        // Проверяем, не был ли уже гетнут этот параметр 
        if (isset($this->decryptedParams[$key])) {
            return $this->decryptedParams[$key];
        }

        // расшифровываем
        $value = $this->crypt->aesDecrypt(Base64::UrlDecode($this->params[$key]));
        // сохраняем расшифрованную версию параметра
        $this->decryptedParams[$key] = $value;

        // и собственно возвращаем
        return $value;
    }

    /**
     * Returns all input params decrypted
     * @return array
     */
    public function all() {
        // обходим все параметры и расшифровываем их
        foreach ($this->params as $key => $value) {
            $this->decryptedParams[$key] = $this->get($key);
        }

        // возвращаем полный массив расшифрованных данных
        return $this->decryptedParams;
    }

    /**
     * Returns only specified input parameters
     * @return array
     */
    public function only() {
        $args = func_get_args();
        $result = array();
        foreach($args as $arg) {
            $result[$arg] = $this->get($arg);
        }

        return $result;
    }

}

Обратите внимание на строку 33: в конструктор передается экземпляр Cryptography. Но согласитесь, неудобно было бы нам постоянно «вручную» инициализировать этот класс, поэтому мы поступим в лучших традициях Laravel — чтобы он сделал все за нас.

Для этого мы проделаем следующее:
  1. Сделаем из DecryptedIntput фасад, точно такой же, как и обычный Input
  2. Сделаем из Cryptography синглтон.
  3. «Зарегистрируем» все это дело внутри нашего собственного Service Provider'а.

Итак, идем по порядку. Делаем фасад DecryptedInput. Для этого создаем файл DecryptedInput.php в app/lib/MySecureApp/Facades:
<?php namespace MySecureApp\Facades;

use Illuminate\Support\Facades\Facade;

class DecryptedInput extends Facade {

    protected static function getFacadeAccessor()
    {
        // "ключ доступа", по которому будет извлекаться
        // DecryptedInput из контейнера
        return 'decryptedinput';
    }

}

Возможно у Вас возникнет путаница в именах: у нас есть два класса с именем DecryptedInput: один — аналог Input'а, другой — его фасад, просто у них разные пространства имен. Поэтому наверное фасад логичнее было бы переименовать в DecryptedInputFacade. Но это только информация на заметку — решать вам. Благодаря пространствам имен мы всегда можем точно указать, какой класс собираемся использовать.

Теперь у нас все готово к тому, чтобы написать собственный Service Provider (пишу это по-английски, так как не придумал пока достойного перевода этого термина, дословно это будет поставщик услуг, но сервис провайдер мне больше нравится). Создадим файл CryptoServiceProvider.php в app/lib/MySecureApp/Providers со следующим содержимым:
<?php namespace MySecureApp\Providers;

use Illuminate\Foundation\AliasLoader;
use Illuminate\Support\ServiceProvider;
use MySecureApp\Cryptography\Cryptography;
use MySecureApp\Cryptography\DecryptedInput;

class CryptoServiceProvider extends ServiceProvider {

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        // Регистрируем синглтон Cryptograpgy
        $this->app->singleton('cryptography', function() {
            return new Cryptography();
        });

        // Регистрируем наш аналог Input'а по ключу 'decryptedinput'
        $this->app['decryptedinput'] = $this->app->share(function($app) {
            return new DecryptedInput($app['cryptography']);
        });

        // Регистрируем алиас на фасад DirectInput'а
        $this->app->booting(function() {
            $loader = AliasLoader::getInstance();
            $loader->alias('DecryptedInput', 'MySecureApp\Facades\DecryptedInput');
        });
    }
}

Ну что, могу поздравить нас с тем, что половину работы мы проделали. Осталось еще столько же. Шучу, чуть меньше… В действительности к данному моменту мы лишь подготовили арсенал инструментов, которые нам еще предстоит использовать.

Можно передохнуть. А пока я вкратце расскажу, как это работает. Начну с DecryptedInput'а. Теперь мы можем использовать его так:
    // ...
    $email = DecryptedInput::get('email');
    $password = DecryptedInput::get('password');

    // или ...
    extract(DecryptedInput::only('email', 'password'));
    // при этом создадутся 2 локальные переменные:
    // $email и $password

Как это происходит? Благодаря алиасу, зарегистрированному в провайдере, Laravel знает, что при обращении к классу DecryptedInput нужно использовать сделанный нами фасад. А как работает фасад? Благодаря возвращаемому методом getFacadeAccessor() ключу (аксессору) 'decryptedinput', Laravel знает, что при всех обращениях к статическим методам фасада нужно «дергать» методы класса \MySecureApp\Cryptography\DecryptedInput (ключ 'decryptedinput' зарегистрирован провайдером):
        // ...
        $this->app['decryptedinput'] = $this->app->share(function($app) {
            // Здесь под DecryptedInput подразумевается именно аналог
            // Input'а, поскольку а) фасад еще не зарегистрирован,
            // и б) в начала провайдера мы прописали
            // use MySecureApp/Cryptography/DecryptedInput
            // Обратите также внимание, что в конструктор передается
            // синглтон Cryptography, зарегистрированный выше
            return new DecryptedInput($app['cryptography']);
        });
        // ...

Ну что, можем продолжать :) Как я уже упоминал, всё взаимодействие клиента с сервером происходит через ApiController. В чем особенность этого контроллера?
  1. Ему передаются зашифрованные данные
  2. Он должен возвращать зашифрованный ответ.

Как поступать с получаемыми зашифрованными данными мы уже разобрались — для этого есть DecryptedInput. А как возвращать зашифрованный ответ? Да еще и подписанный? Возможно кому придет в голову в каждом методе контроллера шифровать данные. Но это не удачный подход. Во-первых, нужно во всех методах соблюдать один и тот же формат данных. Во-вторых, это ненужный копи-паст в каждом методе ( я имею ввиду шифрование данных). Тут нам на помощь придет замечательная фишка Laravel — фильтры. А именно, нам нужен всего лишь один after фильтр, который будет шифровать и форматировать все исходящие данные. Таким образом, все методы апи-контроллера будут просто возвращать сами данные в чистом (plain) виде, а after-фильтр их уже будет шифровать и подписывать.

Что же, давайте  напишем этот фильтр. Создайте файл OutgoingCryptFilter.php в app/lib/MySecureApp/Filters со следующим содержимым:
<?php namespace MySecureApp\Filters;

use MySecureApp\Cryptography\Cryptography;
use MySecureApp\Helpers\Base64;

/**
 * Class OutgoingCryptFilter
 * Encrypts and signs the response
 *
 * @package MySecureApp\Filter
 */
class OutgoingCryptFilter {

    private $crypt;

    public function __construct(Cryptography $crypt) {
        // Этот фильтр будет резолвится из контейнера,
        // поэтому нам не нужно заботится о передаче
        // объекта Cryptography - Laravel сделает это самостоятельно
        $this->crypt = $crypt;
    }

    // Сам фильтр
    public function filter($route, $request, $response) {
        // Сначала получим исходные данные, которые вернул
        // метод контроллера
        $content = $response->getOriginalContent();
        if (!is_string($content)) {
            $content = json_encode($content);
        }

        // Зашифруем их
        $content = Base64::UrlEncode($this->crypt->aesEncrypt($content));
        // И сформируем подпись
        $sign = Base64::UrlEncode($this->crypt->rsaSign($content));

        // Вернем стандартизованный объект (ну точнее массив) вида:
        //     'data' => $content, - данные
        //     'sign' => $sign,    - подпись
        // "Вернем" не совсем корректно, мы перезаписываем данные ответа
        $response->setContent(['data' => $content, 'sign' => $sign]);
    }
}

Как видите, фильтр довольно прост. Хочу еще раз обратить внимание на конструктор: он будет вызван при создании фильтра Laravel'ом, а он достаточно умен, чтобы распознать параметры конструктора и подставить требуемый объект.

Теперь надо зарегистрировать этот фильтр. Здесь мудрствовать не будем и воспользуемся предусмотренным для этого местом: app/filters.php:
// Назовем фильтр cryptOut
Route::filter('cryptOut', 'MySecureApp\Filters\OutgoingCryptFilter');

Осталось совсем чуть-чуть. Написать сам контроллер с как минимум двумя методами: инициализация соединения между клиентом и сервером (так называемый хэндшейк) и какой-нить демонстрационный метод, возвращающий данные.

Воспользуюсь примером из собственной жизни. Цель — авторизовать клиентское приложение и вернуть ему информацию о том, разрешено ли ему выполняться (что-то вроде проверки лицензии).

Итак, вот структура контроллера ApiController:
<?php

use MySecureApp\Cryptography\Cryptography;

class ApiController extends BaseController {

    /**
     * @var Crypt
     */
    private $crypt;

    public function __construct(Cryptography $crypt) {
        $this->crypt = $crypt;

        // добавляем after-фильтр (или если хотите исходящий фильтр)
        // обратите внимание на второй аргумент: массив с полем except:
        // мы не хотим шифровать ответ метода postInit, поскольку на этом
        // этапе наша криптография еще не готова
        $this->afterFilter('cryptOut', array('except'   => 'postInit'));
    }

    // Инициализация (хэндшейк) между клиентом и сервером
    // Это первый этап: клиент передает нам RSA-зашифрованный
    // ключ к AES шифратору. Ключ состоит из двух переменных: key и iv
    public function postInit() {
        // Проверяем, пришел ли ключ
        if (!(Input::has('key') && Input::has('iv'))) {
            return 'ERROR 1';
        }

        // Извлекаем ключ в переменные $key и $iv
        extract(Input::only('key', 'iv'));
        // Расшифровываем их
        $key = $this->crypt->rsaDecrypt($key);
        $iv = $this->crypt->rsaDecrypt($iv);

        // Если хотя бы один из них == false (не удалось расшифровать)
        // вернем клиенту ошибку
        if (!($key && $iv)) {
            return 'ERROR 2';
        }

        // Инициализируем AES полученным ключом
        $this->crypt->initAes(array(
            'key'   => $key,
            'iv'    => $iv,
        ));

        return 'OK';
    }
}

Это была инициализация взаимодействия клиента с сервером. В случае успеха, клиенту вернется просто текстовое сообщение OK. В случае ошибки, ERROR. Я сознательно не стал возвращать текстовое сообщение об ошибке — потенциальному взломщику не нужно знать, что тут твориться.

Теперь давайте напишем какой-нибудь метод, который уже требовал бы шифрования. Предлагаю написать авторизацию. Клиент передает нам свой email и password, а в ответ сервер возвращает данные: успешно ли авторизовался клиент или нет, и когда истекает его лицензия. Я ограничусь лишь контроллером и Dto-объектом LoginResponse. Модель каждый может написать сам, для демонстрации это будет лишним.

Для начала создадим базовый класс для всех ответов сервера. app/lib/MySecureApp/Dto/Responses/ResponseBase.php:
<?php namespace MySecureApp\Dto\Responses;

abstract class ResponseBase implements \JsonSerializable {
    // тип ответа
    public $type;
}

Проще некуда. У класса одно только поле — это тип ответа. Благодаря этому полю клиент сможет написать что-то вроде диспетчера пакетов. Теперь напишем конкретный вариант ответа: LoginResponse (app/lib/MySecureApp/Dto/Responses/LoginResponse.php):
<?php namespace MySecureApp\Dto\Responses;

class LoginResponse extends ResponseBase {

    const LOGIN_SUCCESS = true;
    const LOGIN_FAIL = false;

    public $loginResult; // Результат авторизации
    public $expire; // Дата, когда истекает срок действия лицензии

    public function __construct() {
        $this->type = 'login';
        $this->expire = '0000-00-00 00:00:00';
    }

    /**
     * (PHP 5 &gt;= 5.4.0)<br/>
     * Specify data which should be serialized to JSON
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
     * @return mixed data which can be serialized by <b>json_encode</b>,
     * which is a value of any type other than a resource.
     */
    public function jsonSerialize()
    {
        return [
            'type'          => $this->type,
            'loginResult'   => $this->loginResult,
            'expire'        => $this->expire,
        ];
    }
}

Теперь сам метод контроллера postLogin:
    public function postLogin() {
        // получаем расшифрованные креденшелы
        $creds = [
            'email' => DecryptedInput::get('email'),
            'password'  => DecryptedInput::get('password'),
        ];

        $response = new \MySecureApp\Dto\Responses\LoginResponse;

        if (!Auth::attempt($creds, false)) {
            // Если авторизоваться не удалось, выставляем соответствующее значение в loginResult
            $response->loginResult = \MySecureApp\Dto\Responses\LoginResponse::LOGIN_FAIL;
            // и ретурним
            return json_encode($response);
        }
        $response->loginResult = \MySecureApp\Dto\Responses\LoginResponse::LOGIN_SUCCESS;

        $response->expire = Auth::user()->tariffExpire;
        return json_encode($response);
    }

Ну вот собственно и все. Обратите внимание на 4 и 5 строки — мы используем DecryptedInput для получения переданных нам в POSTе данных.

Regards,
Александр [Амега] Егоров.

P.S. Чуть не забыл, я же обещал рассказать, как можно изменить Cryptography для большей гибкости. Проблема данного кода в том, что он полностью завязан на связке RSA+AES, причем проявляется это даже в именах методов (aesEncrypt, rsaSign и т.д.) А это не есть хорошо. Всякое может быть — вдруг придется отказаться от этих двух алгоритмов и использовать другие?

Как можно исправить ситуацию (я приведу только теорию, без кода — оставлю это вам как домашнее задание)?

Во-первых, создать универсальный интерфейс для класса, предоставляющего функционал для шифрования/дешифрования. С методами наподобие asymmetricDecrypt, symmetricEncrypt и т.п. А текущий класс Cryptography унаследовать от этого интерфейса и переименовать его в более конкретный RsaAesCryptography.

Во-вторых, зарегистрировать этот интерфейс в контейнере App. И везде, где идет инъекция в текущем варианте Cryptography, заменить на CryptographyInterface.

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

Вот теперь вроде все.

P.P.S. Забыл кое-что: добавить маршрут. В app/routes.php:
Route::controller('api', 'ApiController');

и зарегистрировать наш Service Provider. В app/config/app.php:

	'providers' => array(
            // ...
            'MySecureApp\Providers\CryptoServiceProvider',
	),


UPD: написал готовый пакет для реализации всего вышеописанного: Amegatron/Cryptoapi
  • +12
  • 14.9k
  • 9
Share post

Similar posts

Comments 9

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


    Што? С симметричными-асимметричными алгоритмами не попутали?
      0
      Да, извиняюсь, попутал ) Ночью писАл ) Исправил
      0
      GPG-подпись очередной лицензии не? Или обязательно свой велосипед изобретать?
        0
        Лицензия — это частный случай. Здесь проверка лицензии только в качества примера. Какими данным клиент будет обмениваться с сервером зависит от приложения.
          0
          Совершенно правильно. И gpg позволяет обмениваться не только лицензиями, но и другими данными.

          Или обязательно свой велосипед изобретать?
            +1
            Во-первых, RSA и AES — это никакой не велосипед.
            Во-вторых, я же написал в конце, что делать, если вдруг RSA и AES не устраивают. Если лично вам по душе больше GPG — флаг в руки. А эта статья не только о том, как непосредственно юзать RSA+AES. А о том, как это можно организовать в Laravel'е.
            В-третьих, если бы я написал все с точность наоборот (в смысле юзал GPG), нашелся бы тоже человек, который бы посчитал, что я изобрел велосипед, ведь есть phpseclib.
            В-четвертых, phpseclib идет из коробки, в то время как gnupg нужно ставить дополнительно, и не факт, что на вашем хостинге он вообще будет.
            И, наконец, в-пятых — о вкусах не спорят.
              0
              ни кипятись… все нормально.
        0
        Отличная статья! Спасибо.
          0
          Написал пакет для реализации всего описанного. Ссылка в конце статьи.

          Only users with full accounts can post comments. Log in, please.