📜 ⬆️ ⬇️

Encrypted client-server communication on Laravel 4

Introduction


Good day to all! In my first article on Laravel, I want to share my experience in organizing encrypted communication between a client (desktop application) and a server running Laravel.

What is the essence of the problem? There is an application that works on the principle of subscription: paid for on the site, say a month of use and a month of use. Once this period has passed, the application should stop working. Since the condition for this application requires an Internet connection, the best option to check the licensing of the application will be to poll the license server, which can also be a website, through which the payment and extension of the application usage period actually occurs.

But the problem is that if the traffic between the client and the server is not encrypted, anyone can eventually forge the server and use the application for an arbitrarily long time.
')
In this article I will discuss how to organize encrypted communication between the client and the server. As a server platform, there will be an application on the Laravel 4.1 engine (the latest version at the time of publication). It is assumed that an application written in C # will act as a client, but in this article I will not describe the writing of a client. Instead, I can recommend an article from CodeProject that gives an example of using C # cryptography: Encrypting Communication between C # and PHP . Actually this article was the starting point for my research.

I will try to highlight the following questions:

Cryptography basics


All existing encryption algorithms can be divided into 2 types: symmetric and asymmetric encryption. With symmetric encryption, one key is used for both encryption and vice versa, for decryption. Algorithms in this category are much faster asymmetric.

Asymmetric algorithms use two keys: public and private. Encryption is performed using the public key, while decrypting this data can only be done using the private key. In addition to encryption, there is such a thing as a digital signature of data: a hash of data generated using the private key. This hash is verified using the public key. What does this mean and what is it for? This is necessary in order to make sure that the data obtained came from who we expect, and were not modified by anyone on the way to us. Asymmetric algorithms are much slower than symmetric ones and are intended more to encrypt a small amount of data, but more reliably. Therefore, as a rule, the following scheme of interaction between the client and the server is used:
  1. Initially, the client generates a random key for symmetric encryption.
  2. Further, the client encrypts this key using an asymmetric algorithm, using the server's public key known to him in advance.
  3. The encrypted key is sent to the server.
  4. The server receives and decrypts the received key and informs the client with a response that everything is fine.
  5. Further traffic between the server and the client is encrypted using the selected symmetric algorithm.

What is it for? In order to prevent a man-in-the-middle attack (man in the middle) when traffic is intercepted and used by the attacker for his own purposes.

In our case, in addition to encryption, the data will still be signed so that the client is 100% sure that the data was received from his server, and not from the potentially forged data.

Little about the structure of the server application


I will not tell here how to create an empty Laravel application, you can read about it on the framework's official website . Suppose you have already created an application named MySecureApp. All our code will be located inside the app directory. Or rather, in addition to the controllers, models and views, let's create one more thing:
  1. Inside the app folder, create a lib folder, in it MySecureApp - all our classes will be located here that implement the business logic of the application
  2. Inside the app folder, create the keys folder. It will store our private key.
  3. Edit the composer.json file in the application root by adding the following lines:
    "autoload": { "classmap": [ "app/commands", "app/controllers", "app/models", "app/database/migrations", "app/database/seeds", "app/tests/TestCase.php" ], "psr-0": { "MySecureApp": "app/lib" } }, 

    After that, you need to run the command so that the auto-loader sees our classes:
      composer dump-autoload 

  4. Our application itself should have the following directory structure:
      / app
       / lib
         / MySecureApp
           / Cryptography - classes that directly implement encryption
           / Dto - Data Transfer Objects
             / Responses - our API response classes
           / Facades - Facades for convenient access to some classes
           / Filters - filters
           / Helpers - helpers classes
           / Providers - Service providers registering our functionality in the Laravel application. 

    Gradually, we fill these directories with classes.


Client-server interaction


All interaction between the client application and the server will occur through a single controller - ApiController. That is, through Urls of the form mysecureapp / api *

The logic is as follows: the client sends a POST request to the api method of interest, passing each parameter in encrypted form. In response, the server returns a JSON response of the form:
 { "data": "<AES encrypted JSON object>", "sign": "<RSA signature of data>" } 


Cryptography implementation


As a symmetric encryption algorithm will use AES. Asymmetric - RSA. Fortunately, the phpseclib cryptographic library is already included with Laravel, which contains everything we need. Let's start with RSA.

For RSA, we need a pair of keys - open and closed. Well, more precisely if we talk about the server implementation, then you only need the private key. The public key will be needed by the client. Let's generate this key pair.

To do this, we need OpenSSL installed on the computer. As far as I know, on Linux systems it is installed by default. For Windows, you can download it from here: http://slproweb.com/products/Win32OpenSSL.html . Personally, I had difficulties using Light distros - they did not have openssl.cfg necessary for work. Therefore, it is advisable to download and install the full version (~ 19 MB). After installation, you need to create the environment variable OPENSSL_CONF, indicating the above config. This can be done in the console by typing
  set OPENSSL_CONF = \ path \ to \ openssl.cfg 

Let's start creating keys. Run the command line and go (cd) to the directory where you just installed openssl, or rather into the subdirectory bin. To generate the private key, run the following two commands in succession:
  openssl genrsa -aes256 -out temp.key 1024
 openssl rsa -in temp.key -out private.key 

Now, based on the received key, we will generate an X509 certificate, or in other words, the public key:
  openssl req -new -x509 -nodes -sha1 -key private.key -out public.crt -days 365000 

You will be asked a few questions that you don’t need to answer. You can answer anything.
Total we have:
  1. private.key - private key
  2. public.crt - open

Transfer them to the app / keys folder already prepared for this, and add the following line to the application config (app / config / app.php):
 'privateKey' => 'private.key', 

Before proceeding with the implementation of RSA, let's create an auxiliary class for encoding / decoding strings to / from Base64. Create the file app / lib / MySecureApp / Helpers / Base64.php with the following contents:
 <?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)); } } 

Well, now proceed directly to the implementation of RSA. To do this, create a Cryptography class in 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) { // } } 

A small note : the proposed version of the Cryptography class does not quite correspond to the principles of SOLID design. I am doing this intentionally for the purpose of simplifying the material. I will tell you how to improve it at the end of the article.

Let's start filling in the RSA methods. Let's start with rsaInit () . The algorithm is simple: we read the private key passed to us in the parameter, or taken from the config, and initialize the class Crypt_RSA supplied by the phpseclib library:
  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; } 

Now we implement the methods themselves for the work handed over:
  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; } 

Note that the rsaDecrypt method expects the transmitted data to be encoded in Base64. Symmetrically, rsaEncrypt returns encrypted data encoded in Base64.

At this RSA part of the Cryptography class is completed. We proceed to AES.

Add fields to the class:
  /** * AES instance * @var \Crypt_AES */ protected $aes; /** * Whether AES instance is initialized * @var bool */ private $isAesInitialized = false; 

Now the methods are:
  /** * 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) { // ... } 

Initialize 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; } 

Now the data processing methods themselves:
  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); } 

On this class Cryptography is ready. But for now this is only a tool, and how to use it, even though it seems obvious, will be shown further.

Next we need a tool that would give the decrypted incoming data. What I mean? For example, a client wants to log in, and for this he (I am a little ahead of myself), sends a POST request to mysecureapp / api / login with the following data: email = asdpofih345kjafg and password = zxcvzxcvzxcvzxcv - this is AES encrypted data. To get the decrypted data, we need a class similar to the Input facade, but returning the already decrypted data. Name it DecryptedInput and create it in app / lib / MySecureApp / Cryptography / DecryptedInput.php . We implement in it the most popular Input'a methods: get (), all () and 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; } } 

Pay attention to line 33: a copy of Cryptography is passed to the constructor. But you see, it would be inconvenient for us to constantly “manually” initialize this class, so we will proceed in the best traditions of Laravel - so that it does everything for us.

To do this, we do the following:
  1. Let's make a facade from DecryptedIntput , exactly the same as usual Input
  2. Make Cryptography singleton.
  3. “Registering” the whole thing inside our own Service Provider .

So, let's go in order. Making the facade DecryptedInput . To do this, create a file DecryptedInput.php in app / lib / MySecureApp / Facades :
 <?php namespace MySecureApp\Facades; use Illuminate\Support\Facades\Facade; class DecryptedInput extends Facade { protected static function getFacadeAccessor() { // " ",     // DecryptedInput   return 'decryptedinput'; } } 

Perhaps you will have a confusion in the names: we have two classes named DecryptedInput: one is an analogue of Input, the other is its facade, they just have different namespaces. Therefore, it would probably be more logical to rename the facade to DecryptedInputFacade . But this is only information on the note - you decide. Thanks to namespaces, we can always specify exactly which class we are going to use.

Now everything is ready for us to write our own Service Provider (I write it in English, because I haven’t yet come up with a decent translation of this term, literally it will be a service provider, but I prefer the service provider). Create a CryptoServiceProvider.php file in app / lib / MySecureApp / Providers with the following contents:
 <?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'); }); } } 

Well, I can congratulate us on the fact that we have done half the work. There are still as many. Just kidding, a little less ... In fact, by this time we only prepared an arsenal of tools that we still have to use.

You can rest. In the meantime, I will briefly tell you how it works. I'll start with DecryptedInput 'a. Now we can use it like this:
  // ... $email = DecryptedInput::get('email'); $password = DecryptedInput::get('password'); //  ... extract(DecryptedInput::only('email', 'password')); //    2  : // $email  $password 

How does this happen? Thanks to the alias registered in the provider, Laravel knows that when referring to the DecryptedInput class, we need to use the facade we have made. And how does the facade work? Thanks to the key (accessor) ' decryptedinput ' returned by the getFacadeAccessor () method, Laravel knows that for all calls to static facade methods, the methods \ MySecureApp \ Cryptography \ DecryptedInput (the key 'decryptedinput' must be registered by the provider) must be pulled :
  // ... $this->app['decryptedinput'] = $this->app->share(function($app) { //   DecryptedInput    // Input',  )    , //  )      // use MySecureApp/Cryptography/DecryptedInput //   ,     //  Cryptography,   return new DecryptedInput($app['cryptography']); }); // ... 

Well, we can continue :) As I already mentioned, all client interaction with the server occurs through ApiController . What is special about this controller?
  1. Encrypted data is sent to it.
  2. It must return an encrypted response.

We have already figured out how to deal with the received encrypted data - there is a DecryptedInput for this. And how to return the ciphered answer? Yes, and signed? Perhaps someone will come to mind in each method of the controller to encrypt data. But this is not a good approach. First, you need to follow the same data format in all methods. Secondly, it is an unnecessary copy-paste in each method (I mean data encryption). Here we will come to the aid of a great chip Laravel - filters. Namely, we need only one after filter that will encrypt and format all outgoing data. Thus, all methods of the API controller will simply return the data itself in a clean (plain) form, and the after-filter will already encrypt and sign it.

Well, let's write this filter. Create an OutgoingCryptFilter.php file in app / lib / MySecureApp / Filters with the following contents:
 <?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]); } } 

As you can see, the filter is quite simple. I want to once again pay attention to the constructor: it will be called when creating the filter by Laravel, and he is smart enough to recognize the parameters of the constructor and substitute the required object.

Now you need to register this filter. We will not be philosophical here and use the space provided for this: app / filters.php :
 //   cryptOut Route::filter('cryptOut', 'MySecureApp\Filters\OutgoingCryptFilter'); 

Left just a little bit. Write the controller itself with at least two methods: initialization of the connection between the client and the server (the so-called handshake) and which thread is a demonstration method that returns data.

I will use an example from my own life. The goal is to authorize the client application and return to it information about whether it is allowed to run (something like a license check).

So, here is the ApiController controller structure :
 <?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'; } } 

This was the initialization of the client's interaction with the server. If successful, the client will return just a text message OK. In case of error, ERROR. I deliberately did not return the text error message - a potential hacker does not need to know what is going on here.

Now let's write some method that would already require encryption. I propose to write an authorization. The client sends us his email and password, and in return the server returns data: whether the client has successfully logged in or not, and when his license expires. I will be limited only to the controller and Dto-object of LoginResponse . Anyone can write a model himself, for demonstration this will be superfluous.

To begin, create a base class for all server responses. app / lib / MySecureApp / Dto / Responses / ResponseBase.php :
 <?php namespace MySecureApp\Dto\Responses; abstract class ResponseBase implements \JsonSerializable { //   public $type; } 

There is simply no place. For a class, the field alone is the type of response. Thanks to this field, the client will be able to write something like a package manager. Now we ’ll write a specific response: 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, ]; } } 

Now the postLogin controller method itself :
  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); } 

Well, that's all. Pay attention to the 4 and 5 lines - we use DecryptedInput to get the data sent to us in POST.

Regards,
Alexander [Amega] Egorov.

PS I almost forgot, but I promised to tell you how you can change Cryptography for more flexibility. The problem with this code is that it is completely tied up with a bunch of RSA + AES, and this is even manifested in the names of the methods (aesEncrypt, rsaSign, etc.). And this is not good. Anything can happen - suddenly you have to abandon these two algorithms and use others?

How can I correct the situation (I will give only a theory, without a code - I will leave it to you as homework)?

-, , /. asymmetricDecrypt, symmetricEncrypt .. Cryptography RsaAesCryptography .

-, App. , Cryptography , CryptographyInterface.

, , : ( CryptographyInterface) , CryptographyInterface .

.

PPS -: . app/routes.php :
 Route::controller('api', 'ApiController'); 

Service Provider. app/config/app.php :
 'providers' => array( // ... 'MySecureApp\Providers\CryptoServiceProvider', ), 


UPD : : Amegatron/Cryptoapi

Source: https://habr.com/ru/post/209612/


All Articles