Skip to content

Commit

Permalink
Add Encryption support
Browse files Browse the repository at this point in the history
  • Loading branch information
mauriziofonte committed May 11, 2021
1 parent f5c2a74 commit 8cbf9a8
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 23 deletions.
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ try {
$gzip_deflate_encoded = Base62x::encode($payload)->compress('gzip', 'deflate')->get();
$gzip_gzip_encoded = Base62x::encode($payload)->compress('gzip', 'gzip')->get();
$huffman_encoded = Base62x::encode($payload)->compress('huffman')->get();

// you DON'T NEED to call decompress() because the compression method is "saved" inside the
// encoded payload into a "magic string"
$decoded = Base62x::decode($gzip_zlib_encoded)->get();
}
catch(Exception $ex) {
// One Exception of Mfonte/Base62x/Exception/EncodeException
Expand All @@ -143,10 +147,45 @@ catch(Exception $ex) {
}
```

### Encryption

As of version 1.2 this library support **encryption**.

The encryption **can be chained** with compression.

```php
<?php
use Mfonte\Base62x\Base62x;

$payload = 'my_payload';
$key = 'a_very_secret_string';

try {
$encoded_and_crypted = Base62x::encode($payload)->encrypt($key)->get();
$encoded_and_compressed_and_crypted = Base62x::encode($payload)->encrypt($key)->compress()->get();

// to perform decryption, you must pass in the original $key
$decrypted = Base62x::decode($payload)->decrypt($key)->get();
}
catch(Exception $ex) {
// One Exception of Mfonte/Base62x/Exception/EncodeException
// or Mfonte/Base62x/Exception/InvalidParam
// or Mfonte/Base62x/Exception/CompressionException
// or Mfonte/Base62x/Exception/CryptException
}
```

### Testing

Simply run `composer install` over this module's installation directory.

Then, run `./vendor/bin/phpunit --debug tests`

Some tests **may fail** as **testEncodingWithAllAvailableEncryptionAlgorithms** performs a check over all available **openssl_get_cipher_methods()** installed on your environment. A possible example of failure is `Encryption method "id-aes128-CCM" is either unsupported in your PHP installation or not a valid encryption algorithm.`

### TODO

- [ ] Add **Bzip2 compression**
- [ ] Add **Encrytion** over encoded payloads

### Contributing

Expand Down
118 changes: 114 additions & 4 deletions src/Base62x.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

namespace Mfonte\Base62x;

use Exception;
use Mfonte\Base62x\Exception\InvalidParam;
use Mfonte\Base62x\Exception\CryptException;
use Mfonte\Base62x\Exception\DecodeException;
use Mfonte\Base62x\Exception\EncodeException;
use Mfonte\Base62x\Encoding\Base62x as Encoder;
use Mfonte\Base62x\Encryption\Crypt as Crypter;
use Mfonte\Base62x\Compression\Gzip\GzipCompression as GzipCompressor;
use Mfonte\Base62x\Compression\Huffman\HuffmanCoding as HuffmanCompressor;

Expand Down Expand Up @@ -44,6 +47,21 @@ class Base62x
*/
protected $compressEncoding = null;

/**
* The encryption method (algorithm) to be used in case of password-protected encoding.
* This variable *must* be a valid method supported in openssl_get_cipher_methods().
*
* @var string
*/
protected $cryptMethod;

/**
* The encrypt/decrypt key (password) to be used to protect/unprotect the encoding.
*
* @var mixed
*/
protected $cryptKey;

/**
* Wheter the payload needs to be decompressed after the decoding. Defaults to false.
*
Expand Down Expand Up @@ -109,7 +127,10 @@ public function compress($algo = 'gzip', $encoding = 'zlib'): self
if (!\array_key_exists($algo, $this->_validCompressionAlgorithms)) {
throw new InvalidParam('algo', __FUNCTION__, __CLASS__);
}
if (\is_array($this->_validCompressionAlgorithms[$algo]) && !\in_array($encoding, $this->_validCompressionAlgorithms[$algo], true)) {
if (
\is_array($this->_validCompressionAlgorithms[$algo]) &&
!\in_array($encoding, $this->_validCompressionAlgorithms[$algo], true)
) {
throw new InvalidParam('encoding', __FUNCTION__, __CLASS__);
} elseif (!\is_array($this->_validCompressionAlgorithms[$algo])) {
$encoding = null;
Expand All @@ -133,6 +154,39 @@ public function decompress(): self
return $this;
}

/**
* Sets the encryption key (password) and method (algorithm).
*
* @param string $key A password for your encoded base62x output string
* @param string $method A valid openssl cypher method as supported in your environment (openssl_get_cipher_methods)
*
* @return \Mfonte\Base62x\Base62x
*/
public function encrypt(string $key, string $method = 'aes-128-ctr'): self
{
if (!\function_exists('openssl_get_cipher_methods')) {
throw new CryptException('openssl_get_cipher_methods unsupported in your PHP installation');
}
if (!\in_array(\mb_strtolower($method), \openssl_get_cipher_methods(), true)) {
throw new CryptException('Encryption method "'.$method.'" is either unsupported in your PHP installation or not a valid encryption algorithm.');
}

$this->cryptMethod = \mb_strtolower($method);
$this->cryptPassword = $key;

return $this;
}

/**
* Sets the encryption key (password) and method (algorithm).
*
* @see self::encrypt
*/
public function decrypt(string $key, string $method = 'aes-128-ctr'): self
{
return $this->encrypt($key, $method);
}

/**
* Gets the encoded or decoded mixed variable originally passed as $payload to instance.
*
Expand Down Expand Up @@ -163,6 +217,9 @@ public function get()
*/
private function _encode(string $payload): string
{
if ($this->cryptKey && $this->cryptMethod) {
$payload = $this->_performEncryption($payload);
}
if ($this->compressAlgorithm) {
$payload = $this->_performCompress($payload);
}
Expand All @@ -185,12 +242,18 @@ private function _decode(string $payload): string
throw new DecodeException();
}

// remove the magic string for Compression
$data = $this->_getCompressionFootprintAndSanitizePayload($decoded);

if ($data['compression_algo']) {
$decoded = $this->_performUncompress($data['payload'], $data['compression_algo'], $data['compression_encoding']);
}

// eventually perform decryption
if ($this->cryptKey && $this->cryptMethod) {
$decoded = $this->_performDecryption($decoded);
}

return $decoded;
}

Expand Down Expand Up @@ -236,14 +299,61 @@ private function _performUncompress(string $payload, string $compression_algo, ?
}

/**
* Gets a "magic string" that will be appendend at beginning of the compressed payload,
* Performs the actual encryption before chaining it into the Base62x encoder.
*
* @param mixed $payload
*/
private function _performEncryption(string $payload): ?string
{
try {
$crypt = new Crypter([
'key' => $this->cryptKey,
'method' => $this->cryptMethod,
]);

return $crypt->cipher($payload)->encrypt();
} catch (Exception $ex) {
throw new CryptException('Cannot encrypt the payload: '.$ex->getMessage());
}

return null;
}

/**
* Decrypts the payload, that was prior encrypted using the on-board encrypter.
*/
private function _performDecryption(string $payload): string
{
if (empty($this->cryptKey)) {
throw new CryptException('Cannot decrypt the payload without a valid cryptKey');
}
if (empty($this->cryptKey)) {
throw new CryptException('Cannot decrypt the payload without a valid cryptMethod');
}

try {
$crypt = new Crypter([
'key' => $this->cryptKey,
'method' => $this->cryptMethod,
]);

return $crypt->cipher($payload)->decrypt();
} catch (Exception $ex) {
throw new CryptException('Cannot decrypt the payload: '.$ex->getMessage());
}

return null;
}

/**
* Prepares a "magic string" that will be appendend at beginning of the compressed payload,
* prior of chaining it into the Base62x encoder.
* Doing so, the decode method will automagically uncompress the encoded payload, so the subsequent "decode"
* can understand which compression algo+encoding was originally used.
*/
private function _createCompressionFootprint(): string
{
return '['.\base64_encode(\implode(',', [$this->compressAlgorithm, $this->compressEncoding])).']';
return '[MFB62X.COMPRESS.'.\base64_encode(\implode(',', [$this->compressAlgorithm, $this->compressEncoding])).']';
}

/**
Expand All @@ -253,7 +363,7 @@ private function _createCompressionFootprint(): string
private function _getCompressionFootprintAndSanitizePayload(string $payload): array
{
$compression_algo = $compression_encoding = null;
if (\preg_match('/^\[([A-Za-z0-9+\/]+={0,2})\]/', $payload, $match)) {
if (\preg_match('/^\[MFB62X\.COMPRESS\.([A-Za-z0-9+\/]+={0,2})\]/', $payload, $match)) {
list($compression_algo, $compression_encoding) = \explode(',', \base64_decode($match[1], true));
$payload = \preg_replace('/^'.\preg_quote($match[0]).'/', '', $payload, 1);
}
Expand Down
60 changes: 60 additions & 0 deletions src/Encryption/Crypt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Mfonte\Base62x\Encryption;

use Mfonte\Base62x\Exception\CryptException;
use Mfonte\Base62x\Encryption\Cipher\Decrypt;
use Mfonte\Base62x\Encryption\Cipher\Encrypt;

class Crypt
{
protected $method = 'aes-128-ctr'; // default cipher method if none supplied. see: http://php.net/openssl_get_cipher_methods for more.

private $key;

private $data;

public function __construct($options = [])
{
//Set default encryption key if none supplied
$key = isset($options['key']) ? $options['key'] : \php_uname();

$method = isset($options['method']) ? $options['method'] : false;

// convert ASCII keys to binary format
$this->key = \ctype_print($key) ? \openssl_digest($key, 'SHA256', true) : $key;

if ($method) {
if (\in_array(\mb_strtolower($method), \openssl_get_cipher_methods(), true)) {
$this->method = $method;
} else {
throw new CryptException("unrecognised cipher method: {$method}");
}
}
}

public function cipher($data)
{
$this->data = $data;

return $this;
}

public function encrypt()
{
return Encrypt::token(
$this->data,
$this->method,
$this->key
);
}

public function decrypt()
{
return Decrypt::token(
$this->data,
$this->method,
$this->key
);
}
}
11 changes: 11 additions & 0 deletions src/Encryption/Cypher/Bytes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Mfonte\Base62x\Encryption\Cipher;

class Bytes
{
public static function iv($method)
{
return \openssl_cipher_iv_length($method);
}
}
19 changes: 19 additions & 0 deletions src/Encryption/Cypher/Decrypt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Mfonte\Base62x\Encryption\Cipher;

class Decrypt
{
public static function token($data, $method, $key)
{
$iv_strlen = 2 * Bytes::iv($method);
if (\preg_match('/^(.{'.$iv_strlen.'})(.+)$/', $data, $regs)) {
list(, $iv, $crypted_string) = $regs;
if (\ctype_xdigit($iv) && \mb_strlen($iv) % 2 == 0) {
return \openssl_decrypt($crypted_string, $method, $key, 0, \hex2bin($iv));
}
}

return false; // failed to decrypt
}
}
13 changes: 13 additions & 0 deletions src/Encryption/Cypher/Encrypt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Mfonte\Base62x\Encryption\Cipher;

class Encrypt
{
public static function token($data, $method, $key)
{
$iv = \openssl_random_pseudo_bytes(Bytes::iv($method));

return \bin2hex($iv).\openssl_encrypt($data, $method, $key, 0, $iv);
}
}
14 changes: 14 additions & 0 deletions src/Exception/CryptException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Mfonte\Base62x\Exception;

class CryptException extends \RuntimeException
{
const REASON = 'Crypt Exception';
const CODE = 0;

public function __construct(string $reason)
{
parent::__construct('Exception in encryption algorithm : '.$reason, static::CODE);
}
}
Loading

0 comments on commit 8cbf9a8

Please sign in to comment.