Add SodiumHandler encrypter

This commit is contained in:
John Paul E. Balandan, CPA 2020-09-21 19:51:21 +08:00
parent 6b80979f35
commit 0f11b66b01
5 changed files with 279 additions and 3 deletions

View File

@ -31,12 +31,24 @@ class Encryption extends BaseConfig
* --------------------------------------------------------------------------
*
* One of the supported drivers, e.g. 'OpenSSL' or 'Sodium'.
* The default driver, if you don't specify one, is 'OpenSSL'.
*
* @var string
*/
public $driver = 'OpenSSL';
/**
* --------------------------------------------------------------------------
* SodiumHandler's Padding Size
* --------------------------------------------------------------------------
*
* This is the number of bytes that will be padded to the plaintext message
* before it is encrypted. Maximum allowed value is 512 bytes. If none is
* given, it will default to 512.
*
* @var integer
*/
public $blockSize = 512;
/**
* --------------------------------------------------------------------------
* Encryption digest

1
env
View File

@ -91,6 +91,7 @@
# encryption.key =
# encryption.driver = OpenSSL
# encryption.blockSize = 512
# encryption.digest = SHA512
#--------------------------------------------------------------------

View File

@ -64,7 +64,7 @@ abstract class BaseHandler implements EncrypterInterface
$config = $config ?? config('Encryption');
// make the parameters conveniently accessible
foreach ($config as $key => $value)
foreach (get_object_vars($config) as $key => $value)
{
if (property_exists($this, $key))
{
@ -111,6 +111,6 @@ abstract class BaseHandler implements EncrypterInterface
*/
public function __isset($key): bool
{
return in_array($key, ['cipher', 'key'], true);
return property_exists($this, $key);
}
}

View File

@ -0,0 +1,168 @@
<?php
/**
* CodeIgniter
*
* An open source application development framework for PHP
*
* This content is released under the MIT License (MIT)
*
* Copyright (c) 2014-2017 British Columbia Institute of Technology
* Copyright (c) 2019-2020 CodeIgniter Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
* @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 4.0.0
* @filesource
*/
namespace CodeIgniter\Encryption\Handlers;
use CodeIgniter\Encryption\Exceptions\EncryptionException;
/**
* SodiumHandler uses libsodium in encryption.
*
* @see https://github.com/jedisct1/libsodium/issues/392
*/
class SodiumHandler extends BaseHandler
{
/**
* Starter key
*
* @var string
*/
protected $key = '';
/**
* Block size for padding message.
*
* @var integer
*/
protected $blockSize = 512;
/**
* {@inheritDoc}
*/
public function encrypt($data, $params = null)
{
$this->parseParams($params);
if (empty($this->key))
{
throw EncryptionException::forNeedsStarterKey();
}
// create a nonce for this operation
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 24 bytes
// pad to a maximum of 512 bytes chunks
$paddedLength = $this->blockSize <= 512 ? $this->blockSize : 512;
$paddedMessage = sodium_pad($data, $paddedLength);
// encrypt message and combine with nonce
$ciphertext = $nonce . sodium_crypto_secretbox($paddedMessage, $nonce, $this->key);
// cleanup buffers
sodium_memzero($data);
sodium_memzero($this->key);
return $ciphertext;
}
/**
* {@inheritDoc}
*/
public function decrypt($data, $params = null)
{
$this->parseParams($params);
if (empty($this->key))
{
throw EncryptionException::forNeedsStarterKey();
}
if (mb_strlen($data, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES))
{
// message was truncated
throw EncryptionException::forAuthenticationFailed();
}
// Extract info from encrypted data
$nonce = self::substr($data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = self::substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null);
// decrypt and account for extra padding
$paddedLength = $this->blockSize <= 512 ? $this->blockSize : 512;
$paddedPlaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key);
if ($paddedPlaintext === false)
{
// message was tampered in transit
throw EncryptionException::forAuthenticationFailed(); // @codeCoverageIgnore
}
$plaintext = sodium_unpad($paddedPlaintext, $paddedLength);
// cleanup buffers
sodium_memzero($ciphertext);
sodium_memzero($this->key);
return $plaintext;
}
/**
* Parse the $params before doing assignment.
*
* @param array|string|null $params
*
* @throws \CodeIgniter\Encryption\Exceptions\EncryptionException If key is empty
*
* @return void
*/
protected function parseParams($params)
{
if ($params === null)
{
return;
}
if (is_array($params))
{
if (isset($params['key']))
{
$this->key = $params['key'];
}
if (isset($params['block_size']))
{
$this->blockSize = $params['block_size'];
}
return;
}
$this->key = (string) $params;
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace CodeIgniter\Encryption\Handlers;
use CodeIgniter\Encryption\Encryption;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Encryption as EncryptionConfig;
class SodiumHandlerTest extends CIUnitTestCase
{
/**
* @var \CodeIgniter\Encryption\Encryption
*/
protected $encryption;
/**
* @var \Config\Encryption
*/
protected $config;
protected function setUp(): void
{
if (! extension_loaded('sodium'))
{
$this->markTestSkipped('Libsodium is not available.');
}
parent::setUp();
$this->config = new EncryptionConfig();
$this->config->driver = 'Sodium';
$this->config->key = sodium_crypto_secretbox_keygen();
$this->encryption = new Encryption($this->config);
}
public function testPropertiesGetter()
{
$this->config->key = sodium_crypto_secretbox_keygen();
$this->config->blockSize = 256;
$encrypter = $this->encryption->initialize($this->config);
$this->assertSame($this->config->key, $encrypter->key);
$this->assertSame($this->config->blockSize, $encrypter->blockSize);
$this->assertNull($encrypter->driver);
}
public function testEmptyKeyThrowsErrorOnInitialize()
{
$this->expectException('CodeIgniter\Encryption\Exceptions\EncryptionException');
$this->config->key = '';
$this->encryption->initialize($this->config);
}
public function testEmptyKeyThrowsErrorOnEncrypt()
{
$this->expectException('CodeIgniter\Encryption\Exceptions\EncryptionException');
$encrypter = $this->encryption->initialize($this->config);
$encrypter->encrypt('Some message to encrypt', '');
}
public function testEmptyKeyThrowsErrorOnDecrypt()
{
$this->expectException('CodeIgniter\Encryption\Exceptions\EncryptionException');
$encrypter = $this->encryption->initialize($this->config);
$ciphertext = $encrypter->encrypt('Some message to encrypt');
// After encrypt, the message and key are wiped from buffer
$encrypter->decrypt($ciphertext);
}
public function testTruncatedMessageThrowsErrorOnDecrypt()
{
$this->expectException('CodeIgniter\Encryption\Exceptions\EncryptionException');
$encrypter = $this->encryption->initialize($this->config);
$ciphertext = $encrypter->encrypt('Some message to encrypt');
$truncated = mb_substr($ciphertext, 0, 24, '8bit');
$encrypter->decrypt($truncated, ['block_size' => 256, 'key' => sodium_crypto_secretbox_keygen()]);
}
public function testDecryptingMessages()
{
$key = sodium_crypto_secretbox_keygen();
$msg = 'A plaintext message for you.';
$this->config->key = $key;
$encrypter = $this->encryption->initialize($this->config);
$ciphertext = $encrypter->encrypt($msg);
$this->assertSame($msg, $encrypter->decrypt($ciphertext, $key));
$this->assertNotSame('A plain-text message for you.', $encrypter->decrypt($ciphertext, $key));
}
}