Compare commits

..

13 Commits

Author SHA1 Message Date
Clemens Schwaighofer
c010673705 CoreLibs add Security\SymmetricEncryption 2023-05-24 16:00:49 +09:00
Clemens Schwaighofer
b16ff4c613 Release: v8.4.0 2023-05-18 15:21:50 +09:00
Clemens Schwaighofer
e9c791c164 Add better error reporting to DB\IO for query with params
On error with query with params the query was sent to the server and
if ther query itself is ok but there is a problem with the parameters
a wrong error message ($1 not found) will be returned

Add pg_last_error reporting to catch this too.

Update both error reporting to return not string and prefix combined
but prefix + error string in array

In error return check that both strings are not equal, so we do not
return the same error string twice.

Also default set dbh variable in the PgSQL class to false so it will
skip last error report if there is no dbh set yet.

Bug fix for db query with params debug output. if there are more than 9
entries the $1 of eg $10 is replaced with $1 entry again. Changed to
'#' instead '$' to avoid this.

Other:
ACL\Login: replace EOM with HTML
config.master: replace list() with []
Add single DB tester where we can test single db calls without adding
more to the general test run
2023-05-18 15:20:36 +09:00
Clemens Schwaighofer
c7ec1300b7 Published: v8.3.1 2023-04-26 15:43:11 +09:00
Clemens Schwaighofer
064710324e Bug fix in arraySearchKey path reset 2023-04-26 15:41:56 +09:00
Clemens Schwaighofer
e0356dcadf Release: v8.3.0 2023-04-26 14:56:11 +09:00
Clemens Schwaighofer
62a5992e3a Array combined: new arraySearchKey method
Also update publish script and move URLS to .env file
2023-04-26 14:54:13 +09:00
Clemens Schwaighofer
6bb957fcb3 Publish v8.2.2 2023-04-11 11:04:41 +09:00
Clemens Schwaighofer
0c1f060759 Merge branch 'development' 2023-04-11 11:03:24 +09:00
Clemens Schwaighofer
aad46ec80a DB\IO: add missing debug query, clean up not needed code
in dbReturn with params on not matching param the system exited on fail
without printing the query making it hard to find where the error is.
Added debug output in case the params count is not matching.
Same move in the dbExecute call

removed param count check from dbReturnRow/dbReturnArray as this check
is done in the dbExecParams call anyway
2023-04-11 11:03:04 +09:00
Clemens Schwaighofer
f5e9f0610d Publish: v8.2.1 2023-04-10 17:24:47 +09:00
Clemens Schwaighofer
14a5250cd7 DB\IO: Bug fix for missing query params replacement in debug messages 2023-04-10 17:23:27 +09:00
Clemens Schwaighofer
6e6edef57d Release: v8.2.0 2023-04-10 14:38:50 +09:00
17 changed files with 863 additions and 102 deletions

View File

@@ -1 +1 @@
8.1.4
8.4.0

View File

@@ -20,7 +20,11 @@ fi;
# read in the .env.deploy file and we must have
# GITLAB_USER
# GITLAB_TOKEN
# GITLAB_URL
# GITEA_USER
# GITEA_DEPLOY_TOKEN
# GITEA_URL_DL
# GITEA_URL_PUSH
if [ ! -f "${BASE_FOLDER}.env.deploy" ]; then
echo "Deploy enviroment file .env.deploy is missing";
exit;
@@ -33,26 +37,27 @@ set +o allexport;
echo "[START]";
# gitea
if [ ! -z "${GITEA_USER}" ] && [ ! -z "${GITEA_TOKEN}" ]; then
if [ ! -z "${GITEA_URL_DL}" ] && [ ! -z "${GITEA_URL_PUSH}" ] &&
[ ! -z "${GITEA_USER}" ] && [ ! -z "${GITEA_TOKEN}" ]; then
curl -LJO \
--output-dir "${BASE_FOLDER}" \
https://git.egplusww.jp/Composer/CoreLibs-Composer-All/archive/v${VERSION}.zip;
${GITEA_URL_DL}/v${VERSION}.zip;
curl --user ${GITEA_USER}:${GITEA_TOKEN} \
--upload-file "${BASE_FOLDER}/CoreLibs-Composer-All-v${VERSION}.zip" \
https://git.egplusww.jp/api/packages/Composer/composer?version=${VERSION};
${GITEA_URL_PUSH}?version=${VERSION};
echo "${VERSION}" > "${file_last_published}";
else
echo "Missing either GITEA_USER or GITEA_TOKEN environment variable";
fi;
# gitlab
if [ ! -z "${GITLAB_DEPLOY_TOKEN}" ]; then
if [ ! -z "${GITLAB_URL}" ] && [ ! -z "${GITLAB_DEPLOY_TOKEN}" ]; then
curl --data tag=v${VERSION} \
--header "Deploy-Token: ${GITLAB_DEPLOY_TOKEN}" \
"https://gitlab-na.factory.tools/api/v4/projects/950/packages/composer";
"${GITLAB_URL}";
curl --data branch=master \
--header "Deploy-Token: ${GITLAB_DEPLOY_TOKEN}" \
"https://gitlab-na.factory.tools/api/v4/projects/950/packages/composer";
"${GITLAB_URL}";
echo "${VERSION}" > "${file_last_published}";
else
echo "Missing GITLAB_DEPLOY_TOKEN environment variable";

View File

@@ -68,7 +68,7 @@ declare(strict_types=1);
namespace CoreLibs\ACL;
use CoreLibs\Check\Password;
use CoreLibs\Security\Password;
use CoreLibs\Convert\Json;
class Login
@@ -1608,7 +1608,7 @@ class Login
// TODO: submit or JS to set target page as ajax call
// NOTE: for the HTML block I ignore line lengths
// phpcs:disable
$this->login_template['password_change'] = <<<EOM
$this->login_template['password_change'] = <<<HTML
<div id="pw_change_div" class="hidden" style="position: absolute; top: 30px; left: 50px; width: 400px; height: 220px; background-color: white; border: 1px solid black; padding: 25px;">
<table>
<tr><td class="norm" align="center" colspan="2"><h3>{TITLE_PASSWORD_CHANGE}</h3></td></tr>
@@ -1626,7 +1626,7 @@ class Login
</table>
</div>
{PASSWORD_CHANGE_SHOW}
EOM;
HTML;
// phpcs:enable
}
if ($this->password_forgot) {
@@ -1650,7 +1650,7 @@ EOM;
// now check templates
// TODO: submit or JS to set target page as ajax call
if (!$this->login_template['template']) {
$this->login_template['template'] = <<<EOM
$this->login_template['template'] = <<<HTML
<!DOCTYPE html>
<html lang="{LANGUAGE}">
<head>
@@ -1712,7 +1712,7 @@ h3 { font-size: 18px; }
</form>
</body>
</html>
EOM;
HTML;
}
}

View File

@@ -1164,7 +1164,7 @@ class Basic
public function passwordSet(string $password): string
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Check\Password::passwordSet()', E_USER_DEPRECATED);
return \CoreLibs\Check\Password::passwordSet($password);
return \CoreLibs\Security\Password::passwordSet($password);
}
/**
@@ -1177,7 +1177,7 @@ class Basic
public function passwordVerify(string $password, string $hash): bool
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Check\Password::passwordVerify()', E_USER_DEPRECATED);
return \CoreLibs\Check\Password::passwordVerify($password, $hash);
return \CoreLibs\Security\Password::passwordVerify($password, $hash);
}
/**
@@ -1189,7 +1189,7 @@ class Basic
public function passwordRehashCheck(string $hash): bool
{
trigger_error('Method ' . __METHOD__ . ' is deprecated, use \CoreLibs\Check\Password::passwordRehashCheck()', E_USER_DEPRECATED);
return \CoreLibs\Check\Password::passwordRehashCheck($hash);
return \CoreLibs\Security\Password::passwordRehashCheck($hash);
}
// *** BETTER PASSWORD OPTIONS END ***

View File

@@ -1,6 +1,8 @@
<?php
/*
* NOTE: this is deprecated and all moved \CoreLibs\Security\Password
*
* core password set, check and rehash check wrapper functions
*/
@@ -8,6 +10,8 @@ declare(strict_types=1);
namespace CoreLibs\Check;
use CoreLibs\Security\Password as PasswordNew;
class Password
{
/**
@@ -15,13 +19,16 @@ class Password
*
* @param string $password password
* @return string hashed password
* @deprecated v9.0 Moved to \CoreLibs\Security\Password::passwordSet
*/
public static function passwordSet(string $password): string
{
// always use the PHP default for the password
// password options ca be set in the password init,
// but should be kept as default
return password_hash($password, PASSWORD_DEFAULT);
trigger_error(
'Method ' . __METHOD__ . ' is deprecated, use '
. '\CoreLibs\Security\Password::passwordSet',
E_USER_DEPRECATED
);
return PasswordNew::passwordSet($password);
}
/**
@@ -30,14 +37,16 @@ class Password
* @param string $password password
* @param string $hash password hash
* @return bool true or false
* @deprecated v9.0 Moved to \CoreLibs\Security\Password::passwordVerify
*/
public static function passwordVerify(string $password, string $hash): bool
{
if (password_verify($password, $hash)) {
return true;
} else {
return false;
}
trigger_error(
'Method ' . __METHOD__ . ' is deprecated, use '
. '\CoreLibs\Security\Password::passwordVerify',
E_USER_DEPRECATED
);
return PasswordNew::passwordVerify($password, $hash);
}
/**
@@ -45,14 +54,16 @@ class Password
*
* @param string $hash password hash
* @return bool true or false
* @deprecated v9.0 Moved to \CoreLibs\Security\Password::passwordRehashCheck
*/
public static function passwordRehashCheck(string $hash): bool
{
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
return true;
} else {
return false;
}
trigger_error(
'Method ' . __METHOD__ . ' is deprecated, use '
. '\CoreLibs\Security\Password::passwordRehashCheck',
E_USER_DEPRECATED
);
return PasswordNew::passwordRehashCheck($hash);
}
}

View File

@@ -177,6 +177,65 @@ class ArrayHandler
return false;
}
/**
* search for one or many keys in array and return matching values
* If flat is set to true, return flat array with found values only
* If prefix is turned on each found group will be prefixed with the
* search key
*
* @param array<mixed> $array array to search in
* @param array<mixed> $needles keys to find in array
* @param bool $flat [false] Turn on flat output
* @param bool $prefix [false] Prefix found with needle key
* @return array<mixed> Found values
*/
public static function arraySearchKey(
array $array,
array $needles,
bool $flat = false,
bool $prefix = false
): array {
$iterator = new \RecursiveArrayIterator($array);
$recursive = new \RecursiveIteratorIterator(
$iterator,
\RecursiveIteratorIterator::SELF_FIRST
);
$hit_list = [];
if ($prefix === true) {
$hit_list = array_fill_keys($needles, []);
}
$key_path = [];
$prev_depth = 0;
foreach ($recursive as $key => $value) {
if ($prev_depth > $recursive->getDepth()) {
// remove all trailing to ne depth
$diff = $prev_depth - $recursive->getDepth();
array_splice($key_path, -$diff, $diff);
}
$prev_depth = $recursive->getDepth();
if ($flat === false) {
$key_path[$recursive->getDepth()] = $key;
}
if (in_array($key, $needles, true)) {
ksort($key_path);
if ($flat === true) {
$hit = $value;
} else {
$hit = [
'value' => $value,
'path' => $key_path
];
}
if ($prefix === true) {
$hit_list[$key][] = $hit;
} else {
$hit_list[] = $hit;
}
}
}
return $hit_list;
}
/**
* correctly recursive merges as an array as array_merge_recursive
* just glues things together

View File

@@ -735,7 +735,10 @@ class IO
*/
private function __dbErrorPreprocessor(\PgSql\Result|false $cursor = false): array
{
$pg_error_string = '';
$db_prefix = '';
$db_error_string = '';
$db_prefix_last = '';
$db_error_string_last = '';
// 1 = self/__dbErrorPreprocessor, 2 = __dbError, __dbWarning,
// 3+ == actual source
// loop until we get a null, build where called chain
@@ -749,16 +752,31 @@ class IO
if ($where_called === null) {
$where_called = '[Unknown Method]';
}
[$db_prefix_last, $db_error_string_last] = $this->db_functions->__dbPrintLastError();
if ($cursor !== false) {
$pg_error_string = $this->db_functions->__dbPrintError($cursor);
[$db_prefix, $db_error_string] = $this->db_functions->__dbPrintError($cursor);
}
if ($cursor === false && method_exists($this->db_functions, '__dbPrintError')) {
$pg_error_string = $this->db_functions->__dbPrintError();
[$db_prefix, $db_error_string] = $this->db_functions->__dbPrintError();
}
if ($pg_error_string) {
$this->__dbDebug('db', $pg_error_string, 'DB_ERROR', $where_called);
// prefix the master if not the same
if (
!empty($db_error_string_last) &&
trim($db_error_string) != trim($db_error_string_last)
) {
$db_error_string =
$db_prefix_last . ' ' . $db_error_string_last . ';'
. $db_prefix . ' ' . $db_error_string;
} elseif (!empty($db_error_string)) {
$db_error_string = $db_prefix . ' ' . $db_error_string;
}
return [$where_called, $pg_error_string];
if ($db_error_string) {
$this->__dbDebug('db', $db_error_string, 'DB_ERROR', $where_called);
}
return [
$where_called,
$db_error_string
];
}
/**
@@ -902,11 +920,14 @@ class IO
// because the placeholders start with $ and at 1,
// we need to increase each key and prefix it with a $ char
for ($i = 0, $iMax = count($keys); $i < $iMax; $i++) {
$keys[$i] = '$' . ($keys[$i] + 1);
// note: if I use $ here, the str_replace will
// replace it again. eg $11 '$1'1would be replaced with $1 again
// prefix data set with parameter pos
$data[$i] = $keys[$i] . ':' . ($data[$i] === null ?
$data[$i] = '#' . ($keys[$i] + 1) . ':' . ($data[$i] === null ?
'"NULL"' : (string)$data[$i]
);
// search part
$keys[$i] = '$' . ($keys[$i] + 1);
}
// simply replace the $1, $2, ... with the actual data and return it
return str_replace(
@@ -1146,7 +1167,7 @@ class IO
$this->params
),
'__dbPrepareExec',
($this->params === [] ? 'Q' : 'Qp'),
($this->params === [] ? 'Q' : 'Qp')
);
}
// import protection, hash needed
@@ -1166,7 +1187,15 @@ class IO
$this->query_called[$query_hash] > $this->MAX_QUERY_CALL
) {
$this->__dbError(30, false, $this->query);
$this->__dbDebug('db', $this->query, 'dbExec', 'Q[nc]');
$this->__dbDebug(
'db',
$this->__dbDebugPrepare(
$this->query,
$this->params
),
'dbExec',
($this->params === [] ? 'Q[nc]' : 'Qp[nc]')
);
return false;
}
$this->query_called[$query_hash] ++;
@@ -1945,6 +1974,18 @@ class IO
// check if params count matches
// checks if the params count given matches the expected count
if ($this->__dbCheckQueryParams($query, count($params)) === false) {
// in case we got an error print out query
if ($this->db_debug) {
$this->__dbDebug(
'db',
$this->__dbDebugPrepare(
$this->query,
$this->params
),
'dbReturn',
($this->params === [] ? 'Q[e]' : 'Qp[e]')
);
}
return false;
}
// set first call to false
@@ -1968,7 +2009,15 @@ class IO
$this->cursor_ext[$query_hash]['log'][] = 'No cursor';
// for DEBUG, print out each query executed
if ($this->db_debug) {
$this->__dbDebug('db', $this->cursor_ext[$query_hash]['query'], 'dbReturn', 'Q');
$this->__dbDebug(
'db',
$this->__dbDebugPrepare(
$this->cursor_ext[$query_hash]['query'],
$this->cursor_ext[$query_hash]['params']
),
'dbReturn',
($this->cursor_ext[$query_hash]['params'] === [] ? 'Q' : 'Qp'),
);
}
// if no DB Handler try to reconnect
if (!$this->dbh) {
@@ -1997,7 +2046,15 @@ class IO
// if still no cursor ...
if (!$this->cursor_ext[$query_hash]['cursor']) {
if ($this->db_debug) {
$this->__dbDebug('db', $this->cursor_ext[$query_hash]['query'], 'dbReturn', 'Q');
$this->__dbDebug(
'db',
$this->__dbDebugPrepare(
$this->cursor_ext[$query_hash]['query'],
$this->cursor_ext[$query_hash]['params']
),
'dbReturn',
($this->cursor_ext[$query_hash]['params'] === [] ? 'Q[e]' : 'Qp[e]'),
);
}
// internal error handling
$this->__dbError(13, $this->cursor_ext[$query_hash]['cursor']);
@@ -2300,10 +2357,6 @@ class IO
$this->__dbError(17, false, $query);
return false;
}
// checks if the params count given matches the expected count
if ($this->__dbCheckQueryParams($query, count($params)) === false) {
return false;
}
$cursor = $this->dbExecParams($query, $params);
if ($cursor === false) {
return false;
@@ -2348,10 +2401,6 @@ class IO
$this->__dbError(17, false, $query);
return false;
}
// checks if the params count given matches the expected count
if ($this->__dbCheckQueryParams($query, count($params)) === false) {
return false;
}
$cursor = $this->dbExecParams($query, $params);
if ($cursor === false) {
return false;
@@ -2661,6 +2710,17 @@ class IO
);
return false;
}
if ($this->db_debug) {
$this->__dbDebug(
'db',
$this->__dbDebugPrepare(
$this->prepare_cursor[$stm_name]['query'],
$data
),
'dbExecPrep',
'Qpe'
);
}
// if the count does not match
if ($this->prepare_cursor[$stm_name]['count'] != count($data)) {
$this->__dbError(
@@ -2673,17 +2733,6 @@ class IO
);
return false;
}
if ($this->db_debug) {
$this->__dbDebug(
'db',
$this->__dbDebugPrepare(
$this->prepare_cursor[$stm_name]['query'],
$data
),
'dbExecPrep',
'Qp'
);
}
$result = $this->db_functions->__dbExecute($stm_name, $data);
if ($result === false) {
$this->log->debug('ExecuteData', 'ERROR in STM[' . $stm_name . '|'

View File

@@ -209,10 +209,17 @@ interface SqlFunctions
/**
* Undocumented function
*
* @param \PgSql\Result|false $cursor
* @return string
* @return array{0:string,1:string}
*/
public function __dbPrintError(\PgSql\Result|false $cursor = false): string;
public function __dbPrintLastError(): array;
/**
* Undocumented function
*
* @param \PgSql\Result|false $cursor
* @return array{0:string,1:string}
*/
public function __dbPrintError(\PgSql\Result|false $cursor = false): array;
/**
* Undocumented function

View File

@@ -61,7 +61,7 @@ class PgSQL implements Interface\SqlFunctions
/** @var string */
private $last_error_query;
/** @var \PgSql\Connection|false */
private $dbh;
private $dbh = false;
/**
* queries last error query and returns true or false if error was set
@@ -532,18 +532,37 @@ class PgSQL implements Interface\SqlFunctions
return $this->dbh;
}
/**
* Returns last error for active cursor
*
* @return array{0:string,1:string} prefix, error string
*/
public function __dbPrintLastError(): array
{
if (is_bool($this->dbh)) {
return ['', ''];
}
if (!empty($error_message = pg_last_error($this->dbh))) {
return [
'-PostgreSQL-Error-Last-',
$error_message
];
}
return ['', ''];
}
/**
* reads the last error for this cursor and returns
* html formatted string with error name
*
* @param \PgSql\Result|false $cursor cursor
* or null
* @return string error string
* or null
* @return array{0:string,1:string} prefix, error string
*/
public function __dbPrintError(\PgSql\Result|false $cursor = false): string
public function __dbPrintError(\PgSql\Result|false $cursor = false): array
{
if (is_bool($this->dbh)) {
return '';
return ['', ''];
}
// run the query again for the error result here
if ((is_bool($cursor)) && $this->last_error_query) {
@@ -552,10 +571,12 @@ class PgSQL implements Interface\SqlFunctions
$cursor = pg_get_result($this->dbh);
}
if ($cursor && $error_str = pg_result_error($cursor)) {
return '-PostgreSQL-Error- '
. $error_str;
return [
'-PostgreSQL-Error-',
$error_str
];
} else {
return '';
return ['', ''];
}
}

View File

@@ -1954,7 +1954,7 @@ class Generate extends \CoreLibs\DB\Extended\ArrayIO
if ($this->table_array[$key]['value']) {
// use the better new passwordSet instead of crypt based
$this->table_array[$key]['value'] =
\CoreLibs\Check\Password::passwordSet($this->table_array[$key]['value']);
\CoreLibs\Security\Password::passwordSet($this->table_array[$key]['value']);
$this->table_array[$key]['HIDDEN_value'] = $this->table_array[$key]['value'];
} else {
// $this->table_array[$key]['HIDDEN_value'] =

View File

@@ -0,0 +1,61 @@
<?php
/**
* very simple symmetric encryption
* better use: https://paragonie.com/project/halite
*
* this is for creating secret keys for
* Security\SymmetricEncryption
*/
declare(strict_types=1);
namespace CoreLibs\Security;
class CreateKey
{
/**
* Create a random key that is a hex string
*
* @return string Hex string key for encrypting
*/
public static function generateRandomKey(): string
{
return self::bin2hex(self::randomKey());
}
/**
* create a random string as binary to encrypt data
* to store it in clear text in some .env file use bin2hex
*
* @return string Binary string for encryption
*/
public static function randomKey(): string
{
return random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
}
/**
* convert binary key to hex string
*
* @param string $hex_key Convert binary key string to hex
* @return string
*/
public static function bin2hex(string $hex_key): string
{
return sodium_bin2hex($hex_key);
}
/**
* convert hex string to binary key
*
* @param string $string_key Convery hex key string to binary
* @return string
*/
public static function hex2bin(string $string_key): string
{
return sodium_hex2bin($string_key);
}
}
// __END__

59
src/Security/Password.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
/*
* core password set, check and rehash check wrapper functions
*/
declare(strict_types=1);
namespace CoreLibs\Security;
class Password
{
/**
* creates the password hash
*
* @param string $password password
* @return string hashed password
*/
public static function passwordSet(string $password): string
{
// always use the PHP default for the password
// password options ca be set in the password init,
// but should be kept as default
return password_hash($password, PASSWORD_DEFAULT);
}
/**
* checks if the entered password matches the hash
*
* @param string $password password
* @param string $hash password hash
* @return bool true or false
*/
public static function passwordVerify(string $password, string $hash): bool
{
if (password_verify($password, $hash)) {
return true;
} else {
return false;
}
}
/**
* checks if the password needs to be rehashed
*
* @param string $hash password hash
* @return bool true or false
*/
public static function passwordRehashCheck(string $hash): bool
{
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
return true;
} else {
return false;
}
}
}
// __END__

View File

@@ -0,0 +1,96 @@
<?php
/**
* very simple symmetric encryption
* Better use: https://paragonie.com/project/halite
*
* current code is just to encrypt and decrypt
*
* must use a valid encryption key created with
* Secruty\CreateKey class
*/
declare(strict_types=1);
namespace CoreLibs\Security;
use CoreLibs\Security\CreateKey;
use SodiumException;
class SymmetricEncryption
{
/**
* Encrypt a message
*
* @param string $message Message to encrypt
* @param string $key Encryption key (as hex string)
* @return string
* @throws \RangeException
*/
public static function encrypt(string $message, string $key): string
{
try {
$key = CreateKey::hex2bin($key);
} catch (SodiumException $e) {
throw new \Exception('Invalid hex key');
}
if (mb_strlen($key, '8bit') !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
throw new \RangeException(
'Key is not the correct size (must be '
. 'SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes long).'
);
}
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = base64_encode(
$nonce
. sodium_crypto_secretbox(
$message,
$nonce,
$key
)
);
sodium_memzero($message);
sodium_memzero($key);
return $cipher;
}
/**
* Decrypt a message
*
* @param string $encrypted Message encrypted with safeEncrypt()
* @param string $key Encryption key (as hex string)
* @return string
* @throws \Exception
*/
public static function decrypt(string $encrypted, string $key): string
{
try {
$key = CreateKey::hex2bin($key);
} catch (SodiumException $e) {
throw new \Exception('Invalid hex key');
}
$decoded = base64_decode($encrypted);
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plain = false;
try {
$plain = sodium_crypto_secretbox_open(
$ciphertext,
$nonce,
$key
);
} catch (SodiumException $e) {
throw new \Exception('Invalid ciphertext (too short)');
}
if (!is_string($plain)) {
throw new \Exception('Invalid Key');
}
sodium_memzero($ciphertext);
sodium_memzero($key);
return $plain;
}
}
// __END__

View File

@@ -31,6 +31,7 @@ final class CoreLibsCombinedArrayHandlerTest extends TestCase
4,
'b',
'c' => 'test',
'single' => 'single',
'same' => 'same',
'deep' => [
'sub' => [
@@ -288,6 +289,188 @@ final class CoreLibsCombinedArrayHandlerTest extends TestCase
];
}
/**
* Undocumented function
*
* @return array
*/
public function arraySearchKeyProvider(): array
{
/*
0: search in array
1: search keys
2: flat flag
3: prefix flag
4: expected array
*/
return [
// single
'find single, standard' => [
0 => self::$array,
1 => ['single'],
2 => null,
3 => null,
4 => [
0 => [
'value' => 'single',
'path' => ['single'],
],
],
],
'find single, prefix' => [
0 => self::$array,
1 => ['single'],
2 => null,
3 => true,
4 => [
'single' => [
0 => [
'value' => 'single',
'path' => ['single'],
],
],
],
],
'find single, flat' => [
0 => self::$array,
1 => ['single'],
2 => true,
3 => null,
4 => [
'single',
],
],
'find single, flat, prefix' => [
0 => self::$array,
1 => ['single'],
2 => true,
3 => true,
4 => [
'single' => [
'single',
],
],
],
// not found
'not found, standard' => [
0 => self::$array,
1 => ['NOT FOUND'],
2 => null,
3 => null,
4 => [],
],
'not found, standard, prefix' => [
0 => self::$array,
1 => ['NOT FOUND'],
2 => null,
3 => true,
4 => [
'NOT FOUND' => [],
],
],
'not found, flat' => [
0 => self::$array,
1 => ['NOT FOUND'],
2 => true,
3 => null,
4 => [],
],
'not found, flat, prefix' => [
0 => self::$array,
1 => ['NOT FOUND'],
2 => true,
3 => true,
4 => [
'NOT FOUND' => [],
],
],
// multi
'multiple found, standard' => [
0 => self::$array,
1 => ['same'],
2 => null,
3 => null,
4 => [
[
'value' => 'same',
'path' => ['a', 'same', ],
],
[
'value' => 'same',
'path' => ['same', ],
],
[
'value' => 'same',
'path' => ['deep', 'sub', 'same', ],
],
]
],
'multiple found, flat' => [
0 => self::$array,
1 => ['same'],
2 => true,
3 => null,
4 => ['same', 'same', 'same', ],
],
// search with multiple
'search multiple, standard' => [
0 => self::$array,
1 => ['single', 'nested'],
2 => null,
3 => null,
4 => [
[
'value' => 'single',
'path' => ['single'],
],
[
'value' => 'bar',
'path' => ['deep', 'sub', 'nested', ],
],
],
],
'search multiple, prefix' => [
0 => self::$array,
1 => ['single', 'nested'],
2 => null,
3 => true,
4 => [
'single' => [
[
'value' => 'single',
'path' => ['single'],
],
],
'nested' => [
[
'value' => 'bar',
'path' => ['deep', 'sub', 'nested', ],
],
],
],
],
'search multiple, flat' => [
0 => self::$array,
1 => ['single', 'nested'],
2 => true,
3 => null,
4 => [
'single', 'bar',
],
],
'search multiple, flat, prefix' => [
0 => self::$array,
1 => ['single', 'nested'],
2 => true,
3 => true,
4 => [
'single' => ['single', ],
'nested' => ['bar', ],
],
],
];
}
/**
* provides array listing for the merge test
*
@@ -691,6 +874,44 @@ final class CoreLibsCombinedArrayHandlerTest extends TestCase
);
}
/**
* Undocumented function
*
* @covers::arraySearchKey
* @dataProvider arraySearchKeyProvider
* @testdox arraySearchKey Search array with keys and flat: $flat, prefix: $prefix [$_dataName]
*
* @param array $input
* @param array $needles
* @param bool|null $flat
* @param bool|null $prefix
* @param array $expected
* @return void
*/
public function testArraySearchKey(
array $input,
array $needles,
?bool $flat,
?bool $prefix,
array $expected
): void {
if ($flat === null && $prefix === null) {
$result = \CoreLibs\Combined\ArrayHandler::arraySearchKey($input, $needles);
} elseif ($flat === null) {
$result = \CoreLibs\Combined\ArrayHandler::arraySearchKey($input, $needles, prefix: $prefix);
} elseif ($prefix === null) {
$result = \CoreLibs\Combined\ArrayHandler::arraySearchKey($input, $needles, flat: $flat);
} else {
$result = \CoreLibs\Combined\ArrayHandler::arraySearchKey($input, $needles, $flat, $prefix);
}
// print "E: " . print_r($expected, true) . "\n";
// print "R: " . print_r($result, true) . "\n";
$this->assertEquals(
$expected,
$result
);
}
/**
* Undocumented function
*

View File

@@ -156,7 +156,7 @@ final class CoreLibsDBIOTest extends TestCase
$db->dbExec("DROP TABLE test_meta");
}
// uid is for internal reference tests
$base_table = <<<EOM
$base_table = <<<SQL
uid VARCHAR,
row_int INT,
row_numeric NUMERIC,
@@ -172,36 +172,36 @@ final class CoreLibsDBIOTest extends TestCase
row_array_varchar VARCHAR ARRAY
)
WITHOUT OIDS
EOM;
SQL;
// create the tables
$db->dbExec(
// primary key name is table + '_id'
<<<EOM
<<<SQL
CREATE TABLE table_with_primary_key (
table_with_primary_key_id SERIAL PRIMARY KEY,
$base_table
EOM
SQL
/* "CREATE TABLE table_with_primary_key ("
// primary key name is table + '_id'
. "table_with_primary_key_id SERIAL PRIMARY KEY, "
. $base_table */
);
$db->dbExec(
<<<EOM
<<<SQL
CREATE TABLE table_without_primary_key (
$base_table
EOM
SQL
/* "CREATE TABLE table_without_primary_key ("
. $base_table */
);
// create simple table for meta test
$db->dbExec(
<<<EOM
<<<SQL
CREATE TABLE test_meta (
row_1 VARCHAR,
row_2 INT
) WITHOUT OIDS
EOM
SQL
/* "CREATE TABLE test_meta ("
. "row_1 VARCHAR, "
. "row_2 INT"
@@ -1342,10 +1342,10 @@ final class CoreLibsDBIOTest extends TestCase
'has default' => false,
'array dims' => 0,
'is enum' => false,
'is base' => 1,
'is base' => true,
'is composite' => false,
'is pesudo' => false,
'description' => '',
'is pseudo' => false
],
'row_2' => [
'num' => 2,
@@ -1355,10 +1355,10 @@ final class CoreLibsDBIOTest extends TestCase
'has default' => false,
'array dims' => 0,
'is enum' => false,
'is base' => 1,
'is base' => true,
'is composite' => false,
'is pesudo' => false,
'description' => '',
'is pseudo' => false
]
]
],
@@ -1374,10 +1374,10 @@ final class CoreLibsDBIOTest extends TestCase
'has default' => false,
'array dims' => 0,
'is enum' => false,
'is base' => 1,
'is base' => true,
'is composite' => false,
'is pesudo' => false,
'description' => '',
'is pseudo' => false
],
'row_2' => [
'num' => 2,
@@ -1387,10 +1387,10 @@ final class CoreLibsDBIOTest extends TestCase
'has default' => false,
'array dims' => 0,
'is enum' => false,
'is base' => 1,
'is base' => true,
'is composite' => false,
'is pesudo' => false,
'description' => '',
'is pseudo' => false
]
]
],
@@ -4425,16 +4425,16 @@ final class CoreLibsDBIOTest extends TestCase
]
]
],
// same but as EOM
'single insert (PK), EOM string' => [
<<<EOM
// same but as heredoc
'single insert (PK), heredoc string' => [
<<<SQL
INSERT INTO table_with_primary_key (
row_varchar, row_varchar_literal, row_int, row_date
) VALUES (
'Text', 'Other', 123, '2022-03-01'
)
RETURNING row_varchar, row_varchar_literal, row_int, row_date
EOM,
SQL,
null,
null,
null,
@@ -4529,16 +4529,16 @@ final class CoreLibsDBIOTest extends TestCase
]
]
],
// same as above but as EOM string
'single insert (No PK), EOM string' => [
<<<EOM
// same as above but as heredoc string
'single insert (No PK), heredoc string' => [
<<<SQL
INSERT INTO table_without_primary_key (
row_varchar, row_varchar_literal, row_int, row_date
) VALUES (
'Text', 'Other', 123, '2022-03-01'
)
RETURNING row_varchar, row_varchar_literal, row_int, row_date
EOM,
SQL,
null,
null,
null,

View File

@@ -7,11 +7,11 @@ namespace tests;
use PHPUnit\Framework\TestCase;
/**
* Test class for Check\Password
* @coversDefaultClass \CoreLibs\Check\Password
* @testdox \CoreLibs\Check\Password method tests
* Test class for Security\Password
* @coversDefaultClass \CoreLibs\Security\Password
* @testdox \CoreLibs\Security\Password method tests
*/
final class CoreLibsCheckPasswordTest extends TestCase
final class CoreLibsSecurityPasswordTest extends TestCase
{
public function passwordProvider(): array
{
@@ -46,7 +46,7 @@ final class CoreLibsCheckPasswordTest extends TestCase
{
$this->assertEquals(
$expected,
\CoreLibs\Check\Password::passwordVerify($input, \CoreLibs\Check\Password::passwordSet($input_hash))
\CoreLibs\Security\Password::passwordVerify($input, \CoreLibs\Security\Password::passwordSet($input_hash))
);
}
@@ -65,7 +65,7 @@ final class CoreLibsCheckPasswordTest extends TestCase
{
$this->assertEquals(
$expected,
\CoreLibs\Check\Password::passwordRehashCheck($input)
\CoreLibs\Security\Password::passwordRehashCheck($input)
);
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use CoreLibs\Security\CreateKey;
use CoreLibs\Security\SymmetricEncryption;
/**
* Test class for Security\SymmetricEncryption and Security\CreateKey
* @coversDefaultClass \CoreLibs\Security\SymmetricEncryption
* @testdox \CoreLibs\Security\SymmetricEncryption method tests
*/
final class CoreLibsSecuritySymmetricEncryption extends TestCase
{
/**
* Undocumented function
*
* @return array
*/
public function providerEncryptDecryptSuccess(): array
{
return [
'valid string' => [
'input' => 'I am a secret',
'expected' => 'I am a secret',
],
];
}
/**
* test encrypt/decrypt produce correct output
*
* @covers ::generateRandomKey
* @covers ::encrypt
* @covers ::decrypt
* @dataProvider providerEncryptDecryptSuccess
* @testdox encrypt/decrypt $input must be $expected [$_dataName]
*
* @param string $input
* @param string $expected
* @return void
*/
public function testEncryptDecryptSuccess(string $input, string $expected): void
{
$key = CreateKey::generateRandomKey();
$encrypted = SymmetricEncryption::encrypt($input, $key);
$decrypted = SymmetricEncryption::decrypt($encrypted, $key);
$this->assertEquals(
$expected,
$decrypted
);
}
/**
* Undocumented function
*
* @return array
*/
public function providerEncryptFailed(): array
{
return [
'wrong decryption key' => [
'input' => 'I am a secret',
'excpetion_message' => 'Invalid Key'
],
];
}
/**
* Test decryption with wrong key
*
* @covers ::generateRandomKey
* @covers ::encrypt
* @covers ::decrypt
* @dataProvider providerEncryptFailed
* @testdox decrypt with wrong key $input throws $exception_message [$_dataName]
*
* @param string $input
* @param string $exception_message
* @return void
*/
public function testEncryptFailed(string $input, string $exception_message): void
{
$key = CreateKey::generateRandomKey();
$encrypted = SymmetricEncryption::encrypt($input, $key);
$wrong_key = CreateKey::generateRandomKey();
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::decrypt($encrypted, $wrong_key);
}
/**
* Undocumented function
*
* @return array
*/
public function providerWrongKey(): array
{
return [
'not hex key' => [
'key' => 'not_a_hex_key',
'exception_message' => 'Invalid hex key'
],
'too short hex key' => [
'key' => '1cabd5cba9e042f12522f4ff2de5c31d233b',
'excpetion_message' => 'Key is not the correct size (must be '
. 'SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes long).'
],
];
}
/**
* test invalid key provided to decrypt or encrypt
*
* @covers ::encrypt
* @covers ::decrypt
* @dataProvider providerWrongKey
* @testdox wrong key $key throws $exception_message [$_dataName]
*
* @param string $key
* @param string $exception_message
* @return void
*/
public function testWrongKey(string $key, string $exception_message): void
{
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::encrypt('test', $key);
// we must encrypt valid thing first so we can fail with the wrong kjey
$enc_key = CreateKey::generateRandomKey();
$encrypted = SymmetricEncryption::encrypt('test', $enc_key);
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::decrypt($encrypted, $key);
}
/**
* Undocumented function
*
* @return array
*/
public function providerWrongCiphertext(): array
{
return [
'too short ciphertext' => [
'input' => 'short',
'exception_message' => 'Invalid ciphertext (too short)'
],
];
}
/**
* Undocumented function
*
* @covers ::decrypt
* @dataProvider providerWrongCiphertext
* @testdox too short ciphertext $input throws $exception_message [$_dataName]
*
* @param string $input
* @param string $exception_message
* @return void
*/
public function testWrongCiphertext(string $input, string $exception_message): void
{
$key = CreateKey::generateRandomKey();
$this->expectExceptionMessage($exception_message);
SymmetricEncryption::decrypt($input, $key);
}
}
// __END__