The goal of this open source package is security through obscurity.
It aims to offer an alternative to delivering your closed source projects in plain text. Instead, you can opt to deliver your files encrypted, alongside a binary PHP extension which will decrypt them on the fly.
This package uses symmetric encryption, which means that the key itself has to be embedded within the binary PHP extension. However, the code for the extension which holds the key and handles the decryption is open source (and is included within this package).
The key is generated by you and only known to you (as the developer). It can be unique per project and/or customer which opens up the possibility of additional functionality later down the line, such as setting expiry dates (for free trials) and/or whitelisting by IP/MAC address.
- macOS
- PHP 8.1
- Laravel 10
- Zephir
The below assumes that you're currently in your application's root directory.
$ composer require chr1sc0des/code-encrypter-laravel
$ php artisan vendor:publish --tag=code-encrypter-config
You should be able to get started with Zephir by downloading the latest release PHAR from GitHub, followed by these commands:
$ cd ~ && pecl install zephir_parser
$ cd ~/Downloads && mv zephir.phar /usr/local/bin/zephir && chmod +x /usr/local/bin/zephir
Again, the below assumes that you're currently in your application's root directory.
$ php artisan make:controller HelloWorld --invokable
π ./app/Http/Controllers/HelloWorld.php
<?php
namespace App\Http\Controllers;
class HelloWorld extends Controller
{
public function __invoke()
{
return 'Hello, World!';
}
}
π ./routes/web.php
<?php
use App\Http\Controllers\HelloWorld;
use Illuminate\Support\Facades\Route;
Route::get('/', HelloWorld::class);
The default configuration file is shown below.
- The
*
wildcard matches all files within the specified directory - The
**
wildcard is recursive and matches all files within the specified directory and subdirectories
π ./config/code-encrypter.php
<?php
return [
'paths' => [
app_path('Http/Controllers/Foo/*'),
app_path('Http/Controllers/Bar/**'),
app_path('Http/Controllers/HelloWorld.php'),
],
'cipher' => config('app.cipher') ?: 'AES-256-CBC',
'minify' => false,
];
A PHP file is considered valid provided that the following conditions are met:
- The extension is: .php
- It contains an opening PHP tag:
<?php
- It does not contain a closing PHP tag:
?>
You should confirm that you can see "Hello, World!" after running the following command:
$ php artisan serve
$ php artisan code:encrypt
The output from the above command will be similar to the following:
Encrypted ....................................................... app/Http/Controllers/HelloWorld.php
[ INFO ] Command successfully completed.
Key ............................................. base64:8dsiWW4Ue016d1TBalTlJAE46vfO91y5/Xu9UFZzhmc=
Cipher .................................................................................. AES-256-CBC
Zephir File .......................................... tmp/code-encrypter/zephir/zephir/encrypter.zep
Zephir Temp Directory (Must Delete) .............................................. tmp/code-encrypter
β Please remember to make a note of your key.
On this run, one file has been successfully encrypted:
π ./app/Http/Controllers/HelloWorld.php
<?php \Zephir\Encrypter::decrypt("c3DHmSPBpjBUQcZ...8+SePRyCXaKbg==", "hj54ztR3KB+7cWKVJFP0QA==");
// base64:eyJpdiI6ImhqNTR6dFIzS0IrN2NXS1...UwZGVhYTY2MDRhIiwidGFnIjoiIn0=
The decrypt()
static method in \Zephir\Encrypter
takes the value and iv (initialization vector) from the payload returned by the encryptString()
method in \Illuminate\Encryption\Encrypter
.
The commented-out base64 encoded string contains the entire JSON payload returned by the encryptString()
method in \Illuminate\Encryption\Encrypter
and is used in the decryption process.
For your convenience, I have published a GitHub Gist which exposes a small number of utilities which will be used throughout the rest of this document.
At this point, you must build the PHP extension because Laravel will no longer be able to find \App\Http\Controllers\HelloWorld
(which will result in an error).
You should switch to the Zephir Temp Directory as detailed on the output from the previous command:
$ cd tmp/code-encrypter
From there, you should initialize Zephir into the zephir directory which has already been created for you:
$ zephir init zephir
Finally, you should step into the zephir directory and build the extension from there:
$ cd zephir && zephir build
Once the extension has been built, you should be prompted to add extension=zephir.so
to your php.ini file. For this, I will use the ini utility exposed by the GitHub Gist:
$ php ~/Downloads/utils.php ini
Next, we can take a look at the Zephir code itself:
π ./tmp/code-encrypter/zephir/zephir/encrypter.zep
namespace Zephir;
class Encrypter
{
public static function decrypt(var gLIEokrXlBB6s, var LxARS8923aokLv)
{
return eval(self::ssjgKzuSCrh9Zjhom1P(gLIEokrXlBB6s, LxARS8923aokLv));
}
...
protected static function ssjgKzuSCrh9Zjhom1P(var gLIEokrXlBB6s, var LxARS8923aokLv)
{
var ZURmZSyHXCWAwv;
let ZURmZSyHXCWAwv = ["f1","db","22","59","6e","14",..."bd","50","56","73","86","67"];
return openssl_decrypt(
gLIEokrXlBB6s,
"AES-256-CBC",
hex2bin(implode(ZURmZSyHXCWAwv)),
0,
base64_decode(LxARS8923aokLv)
);
}
...
}
The above code is not minified, which can (and should) be changed in ./config/code-encrypter.php. During the minifying process, whitespace is randomly added to ensure that the key is never in the same place twice (within the binary PHP extension).
In an effort to further randomize the binary, the \Illuminate\Support\Str::password()
helper is used to name the variables and methods. Additionally, the method which holds the key is shuffled with 4β8 unused methods (each containing their own key). Each key is stored within its own array to avoid being detected by the strings command:
$ strings /path/to/zephir.so
Now, let's use the tmp utility exposed by the GitHub Gist to delete the Zephir Temp Directory:
$ php ~/Downloads/utils.php tmp "/path/to/app/root/"
You should confirm that you are still able to see "Hello, World!" after running the following command:
$ cd ../../../ && php artisan serve
Congratulations π Your encrypted files are now being decrypted on the fly!
In order to decrypt your code, you must supply the key as the first argument:
$ php artisan code:decrypt base64:8dsiWW4Ue016d1TBalTlJAE46vfO91y5/Xu9UFZzhmc=
The output from the above command will be similar to the following:
Decrypted ....................................................... app/Http/Controllers/HelloWorld.php
[ INFO ] Command successfully completed.
On this run, one file has been successfully decrypted:
π ./app/Http/Controllers/HelloWorld.php
<?php
namespace App\Http\Controllers;
class HelloWorld extends Controller
{
public function __invoke()
{
return 'Hello, World!';
}
}
Once the PHP extension has been built by Zephir, you can decrypt and encrypt your code as many times as you like without rebuilding the extension (provided that you use the same key). The key should be supplied via an option to the command:
$ php artisan code:encrypt --key=base64:8dsiWW4Ue016d1TBalTlJAE46vfO91y5/Xu9UFZzhmc=
Note. When encrypting code, the ./tmp/code-encrypter directory will always be generated for you.
At the time of writing, NativePHP ships with a static PHP binary, therefore it is not possible to install additional extensions.
However, as a PoC, we can swap out the NativePHP binary with a copy of our local PHP binary (NativePHP must already be installed). For this, I will use the bin utility exposed by the GitHub Gist:
$ php ~/Downloads/utils.php bin "/path/to/app/root/"
You should confirm that you are still able to see "Hello, World!" after running the following command:
$ php artisan native:serve
Congratulations, again π Your encrypted files are now being decrypted on the fly within NativePHP!
- Aleksandar JevremoviΔ, et al. (2013). Using Cryptology Models for Protecting PHP Source Code.