* Copyright (C) 2026 MDW * * 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 . * 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; }