From c8313550123c43cd3aca28444c813cc126940e99 Mon Sep 17 00:00:00 2001 From: Paolo Date: Wed, 8 Feb 2023 13:33:40 +0100 Subject: [PATCH] - FilterWidget: now it is possible to provide the encryptedColumns parameter to specify which columns of a database table are encrypted, how to cast them and which password to use to decrypt them - Added new constants PGSQL_BYTEA_TYPE, CRYPT_CONF_PASSWORDS, CRYPT_CAST, CRYPT_PASSWORD_NAME, CRYPT_SELECT_TEMPLATE, CRYPT_WHERE_TEMPLATE and CRYPT_WRITE_TEMPLATE to application/core/DB_Model.php - Added new optional parameter encryptedColumns to application/core/DB_Model insert, update, load, loadWhere, execQuery, execReadOnlyQuery and _toPhp - Added new private methods _addEncrypt, _addDecryptQuery and _addDecryptLoad to application/core/DB_Model.php - core/DB_Model: now it is possible to provide the encryptedColumns parameter to specify which columns of a database table are encrypted, how to cast them and which password to use to decrypt them - Adapted application/models/system/PersonLog_model.php --- application/core/DB_Model.php | 322 ++++++++++++------ application/libraries/FilterWidgetLib.php | 23 +- application/models/person/Person_model.php | 19 ++ application/models/system/PersonLog_model.php | 19 +- application/widgets/FilterWidget.php | 33 +- 5 files changed, 314 insertions(+), 102 deletions(-) diff --git a/application/core/DB_Model.php b/application/core/DB_Model.php index 752561615..69e088ef8 100644 --- a/application/core/DB_Model.php +++ b/application/core/DB_Model.php @@ -3,7 +3,7 @@ /** * Copyright (C) 2023 fhcomplete.org * - * This program is free software: you can redistribute it and/or modify + * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. @@ -19,8 +19,6 @@ if (!defined('BASEPATH')) exit('No direct script access allowed'); -use phpseclib\Crypt\Twofish; - /** * */ @@ -46,16 +44,20 @@ class DB_Model extends CI_Model const PGSQL_INT8_TYPE = 'int8'; const PGSQL_FLOAT4_TYPE = 'float4'; const PGSQL_FLOAT8_TYPE = 'float8'; + const PGSQL_BYTEA_TYPE = 'bytea'; // Name of the config entry containing an array of password that can be used to encrypt/decrypt - const ENCRYPTION_PASSWORDS = 'encryption_passwords'; + const CRYPT_CONF_PASSWORDS = 'encryption_passwords'; + const CRYPT_CAST = 'cast'; + const CRYPT_PASSWORD_NAME = 'passwordName'; + const CRYPT_SELECT_TEMPLATE = 'PGP_SYM_DECRYPT(%s, \'%s\')::%s AS %s'; + const CRYPT_WHERE_TEMPLATE = 'PGP_SYM_DECRYPT(%s, \'%s\')::%s'; + const CRYPT_WRITE_TEMPLATE = 'PGP_SYM_ENCRYPT(\'%s\', \'%s\')'; protected $dbTable; // Name of the DB-Table for CI-Insert, -Update, ... protected $pk; // Name of the PrimaryKey for DB-Update, Load, ... protected $hasSequence; // False if this table has a composite primary key that is not using a sequence // True if this table has a primary key that uses a sequence - protected $passwordName; // Eventually the name of the password that is used to encrypt/decrypt - protected $encryptedColumns; // Eventually an array of columns to be encrypted/decrypted private $executedQueryMetaData; private $executedQueryListFields; @@ -78,8 +80,6 @@ class DB_Model extends CI_Model // Set properties $this->hasSequence = true; - $this->passwordName = null; - $this->encryptedColumns = null; $this->debugMode = isset($this->db->db_debug) && $this->db->db_debug === true; // Loads UDF model @@ -114,7 +114,7 @@ class DB_Model extends CI_Model * @param array $data DataArray for Insert * @return array */ - public function insert($data) + public function insert($data, $encryptedColumns = null) { // Check class properties if (is_null($this->dbTable)) return error('The given database table name is not valid', EXIT_MODEL); @@ -122,8 +122,8 @@ class DB_Model extends CI_Model // If this table has UDF and the validation of them is ok if (isError($validate = $this->_prepareUDFsWrite($data, $this->dbTable))) return $validate; - // - $this->_encrypt($data); + // Add the pgp_sym_eccrypt postgresql function to the set clause if needed + $this->_addEncrypt($encryptedColumns, $data); // DB-INSERT $insert = $this->db->insert($this->dbTable, $data); @@ -167,7 +167,7 @@ class DB_Model extends CI_Model * @param array $data DataArray for Insert * @return array */ - public function update($id, $data) + public function update($id, $data, $encryptedColumns = null) { // Check class properties if (is_null($this->pk)) return error('The given primary key is not valid', EXIT_MODEL); @@ -193,8 +193,8 @@ class DB_Model extends CI_Model $this->db->where($tmpId); - // - $this->_encrypt($data); + // Add the pgp_sym_eccrypt postgresql function to the set clause if needed + $this->_addEncrypt($encryptedColumns, $data); // DB-UPDATE $update = $this->db->update($this->dbTable, $data); @@ -259,7 +259,7 @@ class DB_Model extends CI_Model * @param string $id ID (Primary Key) for SELECT ... WHERE * @return array */ - public function load($id = null) + public function load($id = null, $encryptedColumns = null) { // Check class properties if (is_null($this->pk)) return error('The given primary key is not valid', EXIT_MODEL); @@ -280,7 +280,7 @@ class DB_Model extends CI_Model $tmpId = array($this->pk => $id); } - return $this->loadWhere($tmpId); + return $this->loadWhere($tmpId, $encryptedColumns); } /** @@ -288,11 +288,14 @@ class DB_Model extends CI_Model * * @return array */ - public function loadWhere($where = null) + public function loadWhere($where = null, $encryptedColumns = null) { // Check class properties if (is_null($this->dbTable)) return error('The given database table name is not valid', EXIT_MODEL); + // Add the pgp_sym_decrypt postgresql function to the select and where clause if needed + $this->_addDecryptLoad($encryptedColumns, $where); + // Execute query $result = $this->db->get_where($this->dbTable, $where); @@ -300,10 +303,7 @@ class DB_Model extends CI_Model if ($result) { - // Decrypt data if needed - $this->_decrypt($result); - - return success($this->_toPhp($result)); + return success($this->_toPhp($result, $encryptedColumns)); } else { @@ -366,9 +366,6 @@ class DB_Model extends CI_Model // Execute the query $resultDB = $this->db->get_where($this->dbTable, $where); - // Decrypt data if needed - $this->_decrypt($resultDB); - $this->_logLastQuery(); // If everything went ok... @@ -799,7 +796,7 @@ class DB_Model extends CI_Model /** * Like execQuery, but it allows only to perform queries to read data */ - public function execReadOnlyQuery($query, $parametersArray = null) + public function execReadOnlyQuery($query, $parametersArray = null, $encryptedColumns = null) { $result = error('You are allowed to run only query for reading data'); // $cleanedQuery = trim(preg_replace('/\t|\n|\r|;/', '', $query)); // @@ -816,7 +813,7 @@ class DB_Model extends CI_Model { $queryToExec = str_replace(';', '', $query); // - $result = $this->execQuery($queryToExec, $parametersArray); + $result = $this->execQuery($queryToExec, $parametersArray, $encryptedColumns); } return $result; @@ -831,13 +828,16 @@ class DB_Model extends CI_Model * boolean if the query is of the write type (INSERT, UPDATE, DELETE...) * array that represents DB data */ - protected function execQuery($query, $parametersArray = null) + protected function execQuery($query, $parametersArray = null, $encryptedColumns = null) { $result = null; // If the query is empty don't lose time if (!isEmptyString($query)) { + // Add the pgp_sym_decrypt postgresql function to the given query + $this->_addDecryptQuery($encryptedColumns, $query); + // If there are parameters to bind to the query if (is_array($parametersArray) && count($parametersArray) > 0) { @@ -853,7 +853,7 @@ class DB_Model extends CI_Model // If no errors occurred if ($resultDB) { - $result = success($this->_toPhp($resultDB)); + $result = success($this->_toPhp($resultDB, $encryptedColumns)); } else { @@ -895,92 +895,202 @@ class DB_Model extends CI_Model // // - /** - * If the cryptography is enabled and correctly configured + * To add the pgp_sym_encrypt function to the set clause where needed */ - private function _isCryptoEnabledAndValid() + private function _addEncrypt($encryptedColumns, &$data) { - // Get the password list to decrypt/encrypt from the configuration - $encryptionPasswords = $this->config->item(self::ENCRYPTION_PASSWORDS); - - // If not configured or wrongly configured - if (isEmptyArray($encryptionPasswords)) return false; - - // If array of encrypted columns is defined or wrongly defined for this model - if (isEmptyArray($this->encryptedColumns)) return false; - - // If no decryption/encryption password name is defined for this model - if (isEmptyString($this->passwordName)) return false; - - // If the configured password name does not exists in the password list to decrypt/encrypt - if (!array_key_exists($this->passwordName, $encryptionPasswords)) return false; - - // - return true; - } - - /** - * Returns a cipher to decrypt/encrypt data - */ - private function _getCipher() - { - // Set the cipher type - $cipher = new Twofish(); - // Set the password to encrypt using the chosen one - $cipher->setPassword($this->config->item(self::ENCRYPTION_PASSWORDS)[$this->passwordName]); - - return $cipher; - } - - /** - * To encrypt data - * Data is an associative array that contains column names as keys and the colum value as array value - */ - private function _encrypt(&$data) - { - // If no encryption is configured or not correctly configured then exit this method - if (!$this->_isCryptoEnabledAndValid()) return; - - // Gets the cipher - $cipher = $this->_getCipher(); + $tmpData = array(); // Temporary array used to copy not encrypted columns // For each column that is going to be inserted/updated foreach ($data as $column => $value) { // If the current column is in the list of the columns to be encrypted - if (in_array($column, $this->encryptedColumns)) + // and contains the password name element + if (array_key_exists($column, $encryptedColumns) + && array_key_exists(self::CRYPT_PASSWORD_NAME, $encryptedColumns[$column])) { - // Encrypt it! - $data[$column] = base64_encode($cipher->encrypt($value)); + // Password to encrypt data + $encryptionPassword = $this->config->item(self::CRYPT_CONF_PASSWORDS)[ + $encryptedColumns[$column][self::CRYPT_PASSWORD_NAME] + ]; + + // Add the encrypted column to the set clause without escaping + $this->db->set( + $column, + sprintf( + self::CRYPT_WRITE_TEMPLATE, + $value, + $encryptionPassword + ), + false + ); + } + else // otherwise copy this element as it is + { + $tmpData[$column] = $value; + } + } + + $data = $tmpData; // this array does not contain encrypted columns + } + + /** + * To add the pgp_sym_decrypt function to the given query + */ + private function _addDecryptQuery($encryptedColumns, &$query) + { + // If it is request to get encrypted columns + if (!isEmptyArray($encryptedColumns)) + { + // For each requested encrypted column + foreach ($encryptedColumns as $encryptedColumn => $definition) + { + // If the requested encrypted column is well defined + if (!isEmptyArray($definition) + && array_key_exists(self::CRYPT_CAST, $definition) + && array_key_exists(self::CRYPT_PASSWORD_NAME, $definition)) + { + // And if exists the wanted password to decrypt in the configs + if (array_key_exists( + $definition[self::CRYPT_PASSWORD_NAME], + $this->config->item(self::CRYPT_CONF_PASSWORDS)) + ) + { + // Password to decrypt data + $decryptionPassword = $this->config->item(self::CRYPT_CONF_PASSWORDS)[ + $definition[self::CRYPT_PASSWORD_NAME] + ]; + + // Find and replace all the occurrences of the provided encrypted columns + // with the postgresql decryption function + $query = str_replace( + $encryptedColumn, + sprintf( + self::CRYPT_WHERE_TEMPLATE, + $encryptedColumn, + $decryptionPassword, + $definition[self::CRYPT_CAST] + ), + $query + ); + } + } } } } /** - * To decrypt data - * dbResult is an array of objects, each object is a representation of a database record, - * each property of the object is the column name and the property value is the database value + * To add the pgp_sym_decrypt function to the select and where clause where needed */ - private function _decrypt(&$dbResult) + private function _addDecryptLoad($encryptedColumns, &$where) { - // If no encryption is configured or not correctly configured then exit this method - if (!$this->_isCryptoEnabledAndValid()) return; - - // Gets the cipher - $cipher = $this->_getCipher(); - - // For each record from database - foreach ($dbResult->result() as $record) + // If it is request to get encrypted columns + if (!isEmptyArray($encryptedColumns)) { - // For each column to be encrypted - foreach ($this->encryptedColumns as $column) + // For each requested encrypted column + foreach ($encryptedColumns as $encryptedColumn => $definition) { - // If the current record contains such a column - if (property_exists($record, $column)) + // If the requested encrypted column is well defined + if (!isEmptyArray($definition) + && array_key_exists(self::CRYPT_CAST, $definition) + && array_key_exists(self::CRYPT_PASSWORD_NAME, $definition)) { - // Decrypt it! - $record->{$column} = $cipher->decrypt(base64_decode($record->{$column})); + // And if exists the wanted password to decrypt in the configs + if (array_key_exists( + $definition[self::CRYPT_PASSWORD_NAME], + $this->config->item(self::CRYPT_CONF_PASSWORDS)) + ) + { + // Password to decrypt data + $decryptionPassword = $this->config->item(self::CRYPT_CONF_PASSWORDS)[ + $definition[self::CRYPT_PASSWORD_NAME] + ]; + + // ----------------------------------------- + // SELECT + + // Add to the select clause the column to be decrypted + // NOTE: this is going to override any previously added column with the same name + $this->addSelect( + sprintf( + self::CRYPT_SELECT_TEMPLATE, + $encryptedColumn, + $decryptionPassword, + $definition[self::CRYPT_CAST], + $encryptedColumn + ) + ); + + // ----------------------------------------- + // WHERE + + // If the where parameter is a valid array + if (!isEmptyArray($where)) + { + $tmpWhere = array(); + + // For each condition of the where clause + foreach ($where as $column => $condition) + { + $operator = null; // operator not found in the column name + + // Custom operators + if (strpos($column, '>') != false + || strpos($column, '<') != false + || strpos($column, '>=') != false + || strpos($column, '<=') != false + || strpos($column, '!=') != false + || strpos($column, '=') != false + ) + { + $operator = ' '.substr(trim($column), -1).' '; + } + else // default operator + { + $operator = ' = '; + } + + // If the column from the where clause is the same from the encrypted columns definition + if (trim($column) == $encryptedColumn + || ($operator != null && substr(trim($column), 0, strlen(trim($column)) - 2) == $encryptedColumn) + ) + { + // Then rename the column using the postgresql decryption function + $tmpWhere[ + sprintf( + self::CRYPT_WHERE_TEMPLATE, + $encryptedColumn, + $decryptionPassword, + $definition[self::CRYPT_CAST] + ).$operator + ] = $condition; + } + else // otherwise copy the column as it is + { + $tmpWhere[$column] = $condition; + } + } + + $where = $tmpWhere; // replace with the new where + } + // Otherwise if the where parameter is a valid string + elseif (!isEmptyString($where)) + { + // Find and replace all the occurrences of the provided encrypted columns + // with the postgresql decryption function + $where = str_replace( + $encryptedColumn, + sprintf( + self::CRYPT_WHERE_TEMPLATE, + $encryptedColumn, + $decryptionPassword, + $definition[self::CRYPT_CAST] + ), + $where + ); + } + } } } } @@ -1029,7 +1139,7 @@ class DB_Model extends CI_Model * - A FALSE value on failure * - Otherwise an object filled with data on success */ - private function _toPhp($result) + private function _toPhp($result, $encryptedColumns = null) { $udfs = false; // if UDFs are inside the given result set $toPhp = $result; // if there is nothing to convert then return the result from DB @@ -1045,7 +1155,9 @@ class DB_Model extends CI_Model // Looking for booleans, arrays and UDFs foreach ($this->executedQueryMetaData as $eqmd) { - // If array type, boolean type OR a UDF + // If array type, boolean type, numeric type + // Or bytea type + // Or UDF type if (strpos($eqmd->type, DB_Model::PGSQL_ARRAY_TYPE) !== false || $eqmd->type == DB_Model::PGSQL_BOOLEAN_TYPE || $eqmd->type == DB_Model::PGSQL_INT2_TYPE @@ -1053,6 +1165,7 @@ class DB_Model extends CI_Model || $eqmd->type == DB_Model::PGSQL_INT8_TYPE || $eqmd->type == DB_Model::PGSQL_FLOAT4_TYPE || $eqmd->type == DB_Model::PGSQL_FLOAT8_TYPE + || $eqmd->type == DB_Model::PGSQL_BYTEA_TYPE || $this->udflib->isUDFColumn($eqmd->name, $eqmd->type)) { // If UDFs are inside this result set @@ -1115,6 +1228,21 @@ class DB_Model extends CI_Model { $resultElement->{$toBeConverted->name} = $this->pgFloatPhp($resultElement->{$toBeConverted->name}); } + // Byte A type + elseif ($toBeConverted->type == DB_Model::PGSQL_BYTEA_TYPE) + { + // If encrypted columns are defined + // and if the byte a column is defined as encrypted column + if (!isEmptyArray($encryptedColumns) + && array_key_exists($toBeConverted->name, $encryptedColumns)) + { + // keep the column + } + else // otherwise remove the column from the result + { + unset($resultElement->{$toBeConverted->name}); + } + } } } } diff --git a/application/libraries/FilterWidgetLib.php b/application/libraries/FilterWidgetLib.php index 19cdec848..72a749d60 100644 --- a/application/libraries/FilterWidgetLib.php +++ b/application/libraries/FilterWidgetLib.php @@ -1,5 +1,22 @@ . + */ + if (! defined('BASEPATH')) exit('No direct script access allowed'); /** @@ -16,6 +33,7 @@ class FilterWidgetLib const SESSION_SELECTED_FIELDS = 'selectedFields'; const SESSION_COLUMNS_ALIASES = 'columnsAliases'; const SESSION_ADDITIONAL_COLUMNS = 'additionalColumns'; + const SESSION_ENCRYPTED_COLUMNS = 'encryptedColumns'; const SESSION_CHECKBOXES = 'checkboxes'; const SESSION_FILTERS = 'filters'; const SESSION_METADATA = 'datasetMetadata'; @@ -56,6 +74,7 @@ class FilterWidgetLib const ADDITIONAL_COLUMNS = 'additionalColumns'; const CHECKBOXES = 'checkboxes'; const COLUMNS_ALIASES = 'columnsAliases'; + const ENCRYPTED_COLUMNS = 'encryptedColumns'; // ...to format/mark records of a dataset const FORMAT_ROW = 'formatRow'; @@ -367,7 +386,7 @@ class FilterWidgetLib /** * Retrieves the dataset from the DB */ - public function getDataset($datasetQuery) + public function getDataset($datasetQuery, $encryptedColumns) { $dataset = null; @@ -376,7 +395,7 @@ class FilterWidgetLib $this->_ci->load->model('system/Filters_model', 'FiltersModel'); // Execute the given SQL statement suppressing error messages - $dataset = @$this->_ci->FiltersModel->execReadOnlyQuery($datasetQuery); + $dataset = @$this->_ci->FiltersModel->execReadOnlyQuery($datasetQuery, null, $encryptedColumns); } return $dataset; diff --git a/application/models/person/Person_model.php b/application/models/person/Person_model.php index 8875fd4c5..cf99099fe 100644 --- a/application/models/person/Person_model.php +++ b/application/models/person/Person_model.php @@ -1,5 +1,22 @@ . + */ + class Person_model extends DB_Model { /** @@ -8,6 +25,7 @@ class Person_model extends DB_Model public function __construct() { parent::__construct(); + $this->dbTable = 'public.tbl_person'; $this->pk = 'person_id'; @@ -336,3 +354,4 @@ class Person_model extends DB_Model return $this->execQuery($qry, array($person_id, $person_id, $person_id)); } } + diff --git a/application/models/system/PersonLog_model.php b/application/models/system/PersonLog_model.php index 7a66958b8..88b50487a 100644 --- a/application/models/system/PersonLog_model.php +++ b/application/models/system/PersonLog_model.php @@ -1,5 +1,22 @@ . + */ + class PersonLog_model extends DB_Model { /** @@ -17,7 +34,7 @@ class PersonLog_model extends DB_Model * @param array $data Data of Log Entry to save. * @return success object if true */ - public function insert($data) + public function insert($data, $encryptedColumns = null) { $result = $this->db->insert($this->dbTable, $data); if ($result) diff --git a/application/widgets/FilterWidget.php b/application/widgets/FilterWidget.php index 1cef91c15..4c40e434a 100644 --- a/application/widgets/FilterWidget.php +++ b/application/widgets/FilterWidget.php @@ -1,5 +1,22 @@ . + */ + /** * To filter data using a SQL statement */ @@ -59,6 +76,8 @@ class FilterWidget extends Widget private $_sessionTimeout; // session expiring time + private $_encryptedColumns; // contains info about encrypted columns + private static $_FilterWidgetInstance; // static property that contains the instance of itself /** @@ -195,6 +214,7 @@ class FilterWidget extends Widget $this->_formatRow = null; $this->_markRow = null; $this->_checkboxes = null; + $this->_encryptedColumns = null; $this->_hideOptions = null; $this->_hideSelectFields = null; $this->_hideSelectFilters = null; @@ -252,6 +272,14 @@ class FilterWidget extends Widget $this->_additionalColumns = $args[FilterWidgetLib::ADDITIONAL_COLUMNS]; } + // Parameter is used to define the ecrypted columns + if (isset($args[FilterWidgetLib::ENCRYPTED_COLUMNS]) + && is_array($args[FilterWidgetLib::ENCRYPTED_COLUMNS]) + && count($args[FilterWidgetLib::ENCRYPTED_COLUMNS]) > 0) + { + $this->_encryptedColumns = $args[FilterWidgetLib::ENCRYPTED_COLUMNS]; + } + // Parameter is used to add use aliases for the columns fo the dataset if (isset($args[FilterWidgetLib::COLUMNS_ALIASES]) && is_array($args[FilterWidgetLib::COLUMNS_ALIASES]) @@ -441,7 +469,7 @@ class FilterWidget extends Widget ); // Then retrieve dataset from DB - $dataset = $this->filterwidgetlib->getDataset($datasetQuery); + $dataset = $this->filterwidgetlib->getDataset($datasetQuery, $this->_encryptedColumns); // Save changes into session if data are valid if (!isError($dataset)) @@ -476,7 +504,7 @@ class FilterWidget extends Widget $datasetQuery = $this->filterwidgetlib->generateDatasetQuery($this->_query, $parsedFilterJson->filters); // Then retrieve dataset from DB - $dataset = $this->filterwidgetlib->getDataset($datasetQuery); + $dataset = $this->filterwidgetlib->getDataset($datasetQuery, $this->_encryptedColumns); // Try to load the name of the filter using the PhrasesLib $filterName = $this->filterwidgetlib->getFilterName($parsedFilterJson); @@ -497,6 +525,7 @@ class FilterWidget extends Widget FilterWidgetLib::SESSION_SELECTED_FIELDS => $this->_getColumnsNames($parsedFilterJson->columns), // all the selected fields FilterWidgetLib::SESSION_COLUMNS_ALIASES => $this->_columnsAliases, // all the fields aliases FilterWidgetLib::SESSION_ADDITIONAL_COLUMNS => $this->_additionalColumns, // additional columns + FilterWidgetLib::SESSION_ENCRYPTED_COLUMNS => $this->_encryptedColumns, // encrypted columns FilterWidgetLib::SESSION_CHECKBOXES => $this->_checkboxes, // the name of the field used to build the checkboxes column FilterWidgetLib::SESSION_FILTERS => $parsedFilterJson->filters, // all the filters used to filter the dataset FilterWidgetLib::SESSION_METADATA => $this->FiltersModel->getExecutedQueryMetaData(), // the metadata of the dataset