348 lines
14 KiB
PHP
348 lines
14 KiB
PHP
<?php
|
|
/* Copyright (C) 2024 Laurent Destailleur <eldy@users.sourceforge.net>
|
|
* Copyright (C) 2026 MDW <mdeweerd@users.noreply.github.com>
|
|
*
|
|
* 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.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
* or see https://www.gnu.org/
|
|
*/
|
|
|
|
/**
|
|
* \file htdocs/blockedlog/lib/securitycore.lib.php
|
|
* \ingroup core
|
|
* \brief Set of function used for dolibarr security (not common functions).
|
|
* Warning, this file must not depends on other library files, except function.lib.php
|
|
* because it is used at low code level.
|
|
*/
|
|
|
|
define('MAIN_SECURITY_REVERSIBLE_ALGO', 'AES-256-CTR');
|
|
|
|
|
|
/**
|
|
* Return if we are using a HTTPS connection
|
|
* Check HTTPS (no way to be modified by user but may be empty or wrong if user is using a proxy)
|
|
* Take HTTP_X_FORWARDED_PROTO (defined when using proxy)
|
|
* Then HTTP_X_FORWARDED_SSL
|
|
*
|
|
* @return boolean True if user is using HTTPS
|
|
*/
|
|
function isHTTPS()
|
|
{
|
|
$isSecure = false;
|
|
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
|
|
$isSecure = true;
|
|
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') {
|
|
$isSecure = true;
|
|
}
|
|
return $isSecure;
|
|
}
|
|
|
|
/**
|
|
* Encode a string with a symmetric encryption. Used to encrypt sensitive data into database.
|
|
* Note: If a backup is restored onto another instance with a different $conf->file->instance_unique_id, then decoded value will differ.
|
|
* This function is called for example by dol_set_const() when saving a sensible data into database, like into configuration table llx_const, or societe_rib, ...
|
|
*
|
|
* @param string $chain String to encode
|
|
* @param string $key Key to use to decode. It can be a list of keys separated by ','.
|
|
* @param string $ciphering Default ciphering algorithm
|
|
* @param string $forceseed To force the seed. Keep always empty on new versions.
|
|
* @param string $obfuscationmode 'dolcrypt' or 'dolobfuscatev1'
|
|
* @return string Encoded string, with format 'dolcrypt:CIPHERING:seed:cryptedpass'
|
|
* @since v17
|
|
* @see dolDecrypt(), dol_hash()
|
|
*/
|
|
function dolEncrypt($chain, $key = '', $ciphering = '', $forceseed = '', $obfuscationmode = 'dolcrypt')
|
|
{
|
|
global $conf;
|
|
global $dolibarr_disable_dolcrypt_for_debug;
|
|
|
|
if ($chain === '' || is_null($chain)) {
|
|
return '';
|
|
}
|
|
|
|
$reg = array();
|
|
if (preg_match('/^(dolobfuscation|dolcrypt)[^:]*:([^:]+):(.+)$/', $chain, $reg)) {
|
|
// The $chain is already an encrypted string
|
|
return $chain;
|
|
}
|
|
|
|
if (empty($key)) { // This may happen only with $obfuscationmode = 'dolcrypt'
|
|
if (!empty($conf->file->dolcrypt_key)) { // This code was to prepare a renaming of option but has been abandoned. Note: this param was never been set for the moment.
|
|
$key = $conf->file->dolcrypt_key;
|
|
} else {
|
|
// We fall back on the instance_unique_id (coming from $dolibarr_main_instance_unique_id, for backward compatibility).
|
|
$key = $conf->file->instance_unique_id;
|
|
}
|
|
}
|
|
if (empty($ciphering)) {
|
|
$ciphering = constant('MAIN_SECURITY_REVERSIBLE_ALGO');
|
|
}
|
|
|
|
$newchain = $chain;
|
|
|
|
if (function_exists('openssl_encrypt') && empty($dolibarr_disable_dolcrypt_for_debug)) {
|
|
if (empty($key)) {
|
|
return $chain;
|
|
}
|
|
|
|
$ivlen = 16;
|
|
if (function_exists('openssl_cipher_iv_length')) {
|
|
$ivlen = openssl_cipher_iv_length($ciphering);
|
|
}
|
|
if ($ivlen === false || $ivlen < 1 || $ivlen > 32) {
|
|
$ivlen = 16;
|
|
}
|
|
if (empty($forceseed)) {
|
|
$ivseed = dolGetRandomBytes($ivlen);
|
|
} else { // This case has been abandoned
|
|
$ivseed = dol_substr(md5($forceseed), 0, $ivlen, 'ascii', 1);
|
|
}
|
|
|
|
// If $key is a string with several keys, we keep only the first one (the other are alternative to use to decode)
|
|
$key = preg_replace('/,.*$/', '', $key); // Remove content after the ",".
|
|
|
|
$newchain = openssl_encrypt($chain, $ciphering, $key, 0, $ivseed);
|
|
|
|
return $obfuscationmode.':'.$ciphering.':'.$ivseed.':'.$newchain;
|
|
} else {
|
|
return $chain;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode a string with a symmetric encryption. Used to decrypt sensitive data saved into database.
|
|
* Note: If a backup is restored onto another instance with a different $conf->file->instance_unique_id, then decoded value will differ.
|
|
*
|
|
* @param string $chain Encrypted string to decode
|
|
* @param string $key Key to use to decode. It can be a list of keys separated by ','.
|
|
* @param string $patterntotest Pattern to test if decoing is ok.
|
|
* @return string Decrypted string
|
|
* @since v17
|
|
* @see dolEncrypt(), dol_hash()
|
|
*/
|
|
function dolDecrypt($chain, $key = '', $patterntotest = '')
|
|
{
|
|
global $conf;
|
|
|
|
if ($chain === '' || is_null($chain)) {
|
|
return '';
|
|
}
|
|
|
|
$savkey = $key;
|
|
|
|
if (empty($key)) {
|
|
if (!empty($conf->file->dolcrypt_key)) {
|
|
// If dolcrypt_key is defined, we used it in priority. Note: this param has never been set for the moment.
|
|
$key = $conf->file->dolcrypt_key;
|
|
} else {
|
|
// We fall back on the instance_unique_id (coming from $dolibarr_main_instance_unique_id, for backward compatibility).
|
|
$key = !empty($conf->file->instance_unique_id) ? $conf->file->instance_unique_id : "";
|
|
}
|
|
}
|
|
|
|
$reg = array();
|
|
|
|
// Old method (no more used, kept for compatibility)
|
|
if (preg_match('/^crypted:(.+)$/', $chain, $reg)) {
|
|
return dol_decode($reg[1]);
|
|
}
|
|
|
|
// New method
|
|
if (preg_match('/^(dolobfuscation|dolcrypt)[^:]*:([^:]+):(.+)$/', $chain, $reg)) {
|
|
// Do not enable this log, except during debug
|
|
//dol_syslog("We try to decrypt the chain: ".$chain, LOG_DEBUG);
|
|
|
|
$ciphering = $reg[2];
|
|
if (function_exists('openssl_decrypt')) {
|
|
if (empty($key)) {
|
|
dol_syslog("Error dolDecrypt decrypt key is empty", LOG_WARNING);
|
|
return $chain;
|
|
}
|
|
$tmpexplode = explode(':', $reg[3]);
|
|
if (!empty($tmpexplode[1])) {
|
|
$data = $tmpexplode[1];
|
|
$iv = $tmpexplode[0];
|
|
} else {
|
|
$data = (string) $tmpexplode[0];
|
|
$iv = '';
|
|
}
|
|
|
|
$keys = explode(',', $key);
|
|
|
|
$newchain = '';
|
|
|
|
// Loop on each possible keys (usually one, but can be more in future if we have a list of keys)
|
|
foreach ($keys as $tmpkey) {
|
|
$newchain = openssl_decrypt($data, $ciphering, $tmpkey, 0, $iv);
|
|
if (!empty($patterntotest) && preg_match('/^'.preg_quote($patterntotest, '/').'/', $newchain)) {
|
|
break; // decoding is ok, we stop the loop.
|
|
}
|
|
if (ascii_check($newchain)) {
|
|
break; // decoding seems ok, we stop the loop (1rst key is main key, the other one are alternative we can use if we have a pattern to test the decoding).
|
|
}
|
|
}
|
|
|
|
// Test validity of decryption
|
|
//if (!ascii_check($newchain)) {
|
|
if (!ascii_check($newchain) && !utf8_check($newchain)) {
|
|
if (empty($savkey)) {
|
|
dol_syslog("Error dolDecrypt failed: The key dolibarr_main_dolcrypt or dolibarr_main_instance_unique_id, found in conf.php file, seems the one used to encrypt the encrypted string", LOG_ERR);
|
|
} else {
|
|
dol_syslog("Error dolDecrypt failed: The string decoded with the key return a non valid value (not ascii)", LOG_ERR);
|
|
}
|
|
return $chain;
|
|
}
|
|
} else {
|
|
dol_syslog("Error dolDecrypt openssl_decrypt is not available", LOG_ERR);
|
|
return $chain;
|
|
}
|
|
|
|
return $newchain;
|
|
} else {
|
|
return $chain;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a hash (non reversible encryption) of a string.
|
|
* If constant MAIN_SECURITY_HASH_ALGO is defined, we use this function as hashing function (recommended value is 'password_hash')
|
|
* If constant MAIN_SECURITY_SALT is defined, we use it as a salt (used only if hashing algorithm is something else than 'password_hash').
|
|
*
|
|
* @param string $chain String to hash
|
|
* @param 'auto'|'0'|'sha1'|'1'|'sha1md5'|'2'|'md5'|'3'|'openldap'|'4'|'sha256'|'5'|'password_hash'|'6'|'hash' $type Type of hash:
|
|
* 'auto' or '0': will use MAIN_SECURITY_HASH_ALGO else md5
|
|
* 'sha1' or '1': sha1
|
|
* 'sha1md5' or '2': sha1md5
|
|
* 'md5' or '3': md5
|
|
* 'openldapxxx' or '4': for OpenLdap
|
|
* 'sha256' or '5': sha256
|
|
* 'password_hash' or '6': password_hash
|
|
* Use 'md5' if hash is not needed for security purpose. For security need, prefer 'auto'.
|
|
* @param int $nosalt Do not include any salt
|
|
* @param int $mode 0=Return encoded password, 1=Return array with encoding password + encoding algorithm
|
|
* @return string|array{pass_encrypted:string,pass_encoding:string} Hash of string or array with pass_encrypted and pass_encoding
|
|
* @see getRandomPassword(), dol_verifyHash()
|
|
*/
|
|
function dol_hash($chain, $type = '0', $nosalt = 0, $mode = 0)
|
|
{
|
|
// No need to add salt for password_hash
|
|
if (($type == '0' || $type == 'auto') && getDolGlobalString('MAIN_SECURITY_HASH_ALGO') == 'password_hash' && function_exists('password_hash')) {
|
|
// if string contains a null character that can't be encoded. Return an error instead of fatal error.
|
|
if (strpos($chain, "\0") !== false) {
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => 'Invalid string to encrypt. Contains a null character', 'pass_encoding' => '');
|
|
} else {
|
|
return 'Invalid string to encrypt. Contains a null character.';
|
|
}
|
|
}
|
|
|
|
// Build a password hash with default algorithm
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => password_hash($chain, PASSWORD_DEFAULT), 'pass_encoding' => 'password_hash');
|
|
} else {
|
|
return password_hash($chain, PASSWORD_DEFAULT);
|
|
}
|
|
}
|
|
|
|
// Salt value
|
|
if (getDolGlobalString('MAIN_SECURITY_SALT') && $type != '4' && $type !== 'openldap' && empty($nosalt)) {
|
|
$chain = getDolGlobalString('MAIN_SECURITY_SALT') . $chain;
|
|
}
|
|
|
|
if ($type == '1' || $type == 'sha1') {
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => sha1($chain), 'pass_encoding' => 'sha1');
|
|
} else {
|
|
return sha1($chain);
|
|
}
|
|
} elseif ($type == '2' || $type == 'sha1md5') {
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => sha1(md5($chain)), 'pass_encoding' => 'sha1md5');
|
|
} else {
|
|
return sha1(md5($chain));
|
|
}
|
|
} elseif ($type == '3' || $type == 'md5') { // For hashing with no need of security
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => md5($chain), 'pass_encoding' => 'md5');
|
|
} else {
|
|
return md5($chain);
|
|
}
|
|
} elseif ($type == '4' || $type == 'openldap') {
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => dolGetLdapPasswordHash($chain, getDolGlobalString('LDAP_PASSWORD_HASH_TYPE', 'md5')), 'pass_encoding' => 'ldappasswordhash'.getDolGlobalString('LDAP_PASSWORD_HASH_TYPE', 'md5'));
|
|
} else {
|
|
return dolGetLdapPasswordHash($chain, getDolGlobalString('LDAP_PASSWORD_HASH_TYPE', 'md5'));
|
|
}
|
|
} elseif ($type == '5' || $type == 'sha256') {
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => hash('sha256', $chain), 'pass_encoding' => 'sha256');
|
|
} else {
|
|
return hash('sha256', $chain);
|
|
}
|
|
} elseif ($type == '6' || $type == 'password_hash') {
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => password_hash($chain, PASSWORD_DEFAULT), 'pass_encoding' => 'password_hash');
|
|
} else {
|
|
return password_hash($chain, PASSWORD_DEFAULT);
|
|
}
|
|
} elseif (getDolGlobalString('MAIN_SECURITY_HASH_ALGO') == 'sha1') {
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => sha1($chain), 'pass_encoding' => 'sha1');
|
|
} else {
|
|
return sha1($chain);
|
|
}
|
|
} elseif (getDolGlobalString('MAIN_SECURITY_HASH_ALGO') == 'sha1md5') {
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => sha1(md5($chain)), 'pass_encoding' => 'sha1md5');
|
|
} else {
|
|
return sha1(md5($chain));
|
|
}
|
|
}
|
|
|
|
// No particular encoding defined, use default
|
|
if ($mode == 1) {
|
|
return array('pass_encrypted' => md5($chain), 'pass_encoding' => 'md5');
|
|
} else {
|
|
return md5($chain);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute a hash and compare it to the given one
|
|
* For backward compatibility reasons, if the hash is not in the password_hash format, we will try to match against md5 and sha1md5
|
|
* If constant MAIN_SECURITY_HASH_ALGO is defined, we use this function as hashing function.
|
|
* If constant MAIN_SECURITY_SALT is defined, we use it as a salt.
|
|
*
|
|
* @param string $chain String to hash (not hashed string)
|
|
* @param string $hash hash to compare
|
|
* @param 'auto'|'0'|'sha1'|'1'|'sha1md5'|'2'|'md5'|'3'|'openldap'|'4'|'sha256'|'5'|'password_hash'|'6'|'hash' $type Type of hash ('0':auto, '1':sha1, '2':sha1+md5, '3':md5, '4': for OpenLdap, '5':sha256, 'hash'). Use '3' here, if hash is not needed for security purpose, for security need, prefer '0'.
|
|
* @return bool True if the computed hash is the same as the given one
|
|
* @see dol_hash()
|
|
*/
|
|
function dol_verifyHash($chain, $hash, $type = '0')
|
|
{
|
|
if ($type == '0' && getDolGlobalString('MAIN_SECURITY_HASH_ALGO') == 'password_hash' && function_exists('password_verify')) {
|
|
// Try to autodetect which algo we used
|
|
if (! empty($hash[0]) && $hash[0] == '$') {
|
|
return password_verify($chain, $hash);
|
|
} elseif (dol_strlen($hash) == 32) {
|
|
return dol_verifyHash($chain, $hash, '3'); // md5
|
|
} elseif (dol_strlen($hash) == 40) {
|
|
return dol_verifyHash($chain, $hash, '2'); // sha1md5
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return dol_hash($chain, $type) == $hash;
|
|
}
|