278 lines
7.4 KiB
PHP
278 lines
7.4 KiB
PHP
<?php
|
|
/**
|
|
* @package FOF
|
|
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 3, or later
|
|
*/
|
|
|
|
namespace FOF40\Encrypt;
|
|
|
|
defined('_JEXEC') || die;
|
|
|
|
use FOF40\Container\Container;
|
|
|
|
/**
|
|
* Data encryption service for FOF-based components.
|
|
*
|
|
* This service allows you to transparently encrypt and decrypt *text* plaintext data. Use it to provide encryption for
|
|
* sensitive or personal data stored in your database. Please remember:
|
|
*
|
|
* - The default behavior is to create a file with a random key on your component's root. If the file cannot be created
|
|
* the encryption is turned off.
|
|
* - The key file is only created when you access the service. If you never use this service nothing happens (for
|
|
* backwards compatibility).
|
|
* - You have to manually encrypt and decrypt data. It won't happen magically.
|
|
* - Encrypted data cannot be searched unless you implement your own, slow, search algorithm.
|
|
* - Data encryption is meant to be used on top of, not instead of, any other security measures for your site.
|
|
* - Data encryption only protects against exploits targeting the database. If the attacker *also* gains read access to
|
|
* your filesystem OR if the attacker gains read / write access to the filesystem the encryption won't protect you.
|
|
* This is a full compromise of your site. At this point you're pwned and nothing can protect you. If you don't
|
|
* understand this simple truth do NOT use encryption.
|
|
* - This is meant as a simple and basic encryption layer. It has not been independently verified. Use at your own risk.
|
|
*
|
|
* This service has the following FOF application configuration parameters which can be declared under the "container"
|
|
* key (e.g. the "name" attribute of the fof.xml elements under fof > common > container > option):
|
|
*
|
|
* - encrypt_key_file The path to the key file, relative to the component's backend root and WITHOUT the .php extension
|
|
* - encrypt_key_const The constant for the key. By default it is COMPONENTNAME_FOF_ENCRYPT_SERVICE_SECRETKEY where
|
|
* COMPONENTNAME corresponds to the uppercase com_componentname without the com_ prefix.
|
|
*
|
|
* @package FOF40\Encrypt
|
|
*
|
|
* @since 3.3.2
|
|
*/
|
|
class EncryptService
|
|
{
|
|
/**
|
|
* The component's container
|
|
*
|
|
* @var Container
|
|
* @since 3.3.2
|
|
*/
|
|
private $container;
|
|
|
|
/**
|
|
* The encryption engine used by this service
|
|
*
|
|
* @var Aes
|
|
* @since 3.3.2
|
|
*/
|
|
private $aes;
|
|
|
|
/**
|
|
* EncryptService constructor.
|
|
*
|
|
* @param Container $c The FOF component container
|
|
*
|
|
* @since 3.3.2
|
|
*/
|
|
public function __construct(Container $c)
|
|
{
|
|
$this->container = $c;
|
|
$this->initialize();
|
|
}
|
|
|
|
/**
|
|
* Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128###
|
|
*
|
|
* @param string $data The plaintext data
|
|
*
|
|
* @return string The ciphertext, prefixed by ###AES128###
|
|
*
|
|
* @since 3.3.2
|
|
*/
|
|
public function encrypt(string $data): string
|
|
{
|
|
if (!is_object($this->aes))
|
|
{
|
|
return $data;
|
|
}
|
|
|
|
$encrypted = $this->aes->encryptString($data, true);
|
|
|
|
return '###AES128###' . $encrypted;
|
|
}
|
|
|
|
/**
|
|
* Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext.
|
|
*
|
|
* @param string $data The ciphertext, prefixed by ###AES128###
|
|
* @param bool $legacy Use legacy key expansion? Use it to decrypt data encrypted with FOF 3.
|
|
*
|
|
* @return string The plaintext data
|
|
*
|
|
* @since 3.3.2
|
|
*/
|
|
public function decrypt(string $data, bool $legacy = false): string
|
|
{
|
|
if (substr($data, 0, 12) != '###AES128###')
|
|
{
|
|
return $data;
|
|
}
|
|
|
|
$data = substr($data, 12);
|
|
|
|
if (!is_object($this->aes))
|
|
{
|
|
return $data;
|
|
}
|
|
|
|
$decrypted = $this->aes->decryptString($data, true, $legacy);
|
|
|
|
// Decrypted data is null byte padded. We have to remove the padding before proceeding.
|
|
return rtrim($decrypted, "\0");
|
|
}
|
|
|
|
/**
|
|
* Initialize the AES cryptography object
|
|
*
|
|
* @return void
|
|
* @since 3.3.2
|
|
*
|
|
*/
|
|
private function initialize(): void
|
|
{
|
|
if (is_object($this->aes))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$password = $this->getPassword();
|
|
|
|
if (empty($password))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$this->aes = new Aes('cbc');
|
|
$this->aes->setPassword($password);
|
|
}
|
|
|
|
/**
|
|
* Returns the path to the secret key file
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.2
|
|
*/
|
|
private function getPasswordFilePath(): string
|
|
{
|
|
$default = 'encrypt_service_key';
|
|
$baseName = $this->container->appConfig->get('container.encrypt_key_file', $default);
|
|
$baseName = trim($baseName, '/\\');
|
|
|
|
return $this->container->backEndPath . '/' . $baseName . '.php';
|
|
}
|
|
|
|
/**
|
|
* Get the name of the constant where the secret key is stored. Remember that this is searched first, before a new
|
|
* key file is created. You can define this constant anywhere in your code loaded before the encryption service is
|
|
* first used to prevent a key file being created.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.2
|
|
*/
|
|
private function getConstantName(): string
|
|
{
|
|
$default = strtoupper($this->container->bareComponentName) . '_FOF_ENCRYPT_SERVICE_SECRETKEY';
|
|
|
|
return $this->container->appConfig->get('container.encrypt_key_const', $default);
|
|
}
|
|
|
|
/**
|
|
* Returns the password used to encrypt information in the component
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 3.3.2
|
|
*/
|
|
private function getPassword(): string
|
|
{
|
|
$constantName = $this->getConstantName();
|
|
|
|
// If we have already read the file just return the key
|
|
if (defined($constantName))
|
|
{
|
|
return constant($constantName);
|
|
}
|
|
|
|
// Do I have a secret key file?
|
|
$filePath = $this->getPasswordFilePath();
|
|
|
|
// I can't get the path to the file. Cut our losses and assume we can get no key.
|
|
if (empty($filePath))
|
|
{
|
|
define($constantName, '');
|
|
|
|
return '';
|
|
}
|
|
|
|
// If not, try to create one.
|
|
if (!file_exists($filePath))
|
|
{
|
|
$this->makePasswordFile();
|
|
}
|
|
|
|
// We failed to create a new file? Cut our losses and assume we can get no key.
|
|
if (!file_exists($filePath) || !is_readable($filePath))
|
|
{
|
|
define($constantName, '');
|
|
|
|
return '';
|
|
}
|
|
|
|
// Try to include the key file
|
|
include_once $filePath;
|
|
|
|
// The key file contains garbage. Treason! Cut our losses and assume we can get no key.
|
|
if (!defined($constantName))
|
|
{
|
|
define($constantName, '');
|
|
|
|
return '';
|
|
}
|
|
|
|
// Finally, return the key which was defined in the file (happy path).
|
|
return constant($constantName);
|
|
}
|
|
|
|
/**
|
|
* Create a new secret key file using a long, randomly generated password. The password generator uses a crypto-safe
|
|
* pseudorandom number generator (PRNG) to ensure suitability of the password for encrypting data at rest.
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 3.3.2
|
|
*/
|
|
private function makePasswordFile(): void
|
|
{
|
|
// Get the path to the new secret key file.
|
|
$filePath = $this->getPasswordFilePath();
|
|
|
|
// I can't get the path to the file. Sorry.
|
|
if (empty($filePath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$randval = new Randval();
|
|
$secretKey = $randval->getRandomPassword(64);
|
|
$constantName = $this->getConstantName();
|
|
|
|
$fileContent = "<?" . 'ph' . "p\n\n";
|
|
$fileContent .= <<< END
|
|
defined('_JEXEC') or die;
|
|
|
|
/**
|
|
* This file is automatically generated. It contains a secret key used for encrypting data by the component. Please do
|
|
* not remove, edit or manually replace this file. It will render your existing encrypted data unreadable forever.
|
|
*/
|
|
|
|
define('$constantName', '$secretKey');
|
|
|
|
END;
|
|
|
|
$this->container->filesystem->fileWrite($filePath, $fileContent);
|
|
}
|
|
} |