Files
Froxlor/lib/Froxlor/System/Crypt.php
Michael Kaufmann 6777fbf229 type-safe comparsion of md5-compatibility hash-validation
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-02-22 09:35:27 +01:00

288 lines
9.5 KiB
PHP

<?php
/**
* This file is part of the Froxlor project.
* Copyright (c) 2010 the Froxlor Team (see authors).
*
* 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 2
* 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, you can also view it online at
* https://files.froxlor.org/misc/COPYING.txt
*
* @copyright the authors
* @author Froxlor team <team@froxlor.org>
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
*/
namespace Froxlor\System;
use Froxlor\Database\Database;
use Froxlor\Froxlor;
use Froxlor\Settings;
use Froxlor\Validate\Validate;
class Crypt
{
/**
* Generates a random password
*
* @param int $length optional, will be read from settings if not given
* @param bool $isSalt optional, default false, do not include special characters
*
* @return string
*/
public static function generatePassword(int $length = 0, bool $isSalt = false): string
{
$alpha_lower = 'abcdefghijklmnopqrstuvwxyz';
$alpha_upper = strtoupper($alpha_lower);
$numeric = '0123456789';
$special = Settings::Get('panel.password_special_char');
if (empty($length)) {
$length = Settings::Get('panel.password_min_length') > 3 ? Settings::Get('panel.password_min_length') : 10;
}
$pw = self::specialShuffle($alpha_lower);
$n = floor(($length) / 4);
if (Settings::Get('panel.password_alpha_upper')) {
$pw .= mb_substr(self::specialShuffle($alpha_upper), 0, $n);
}
if (Settings::Get('panel.password_numeric')) {
$pw .= mb_substr(self::specialShuffle($numeric), 0, $n);
}
if (Settings::Get('panel.password_special_char_required') && !$isSalt) {
$pw .= mb_substr(self::specialShuffle($special), 0, $n);
}
$pw = mb_substr($pw, -$length);
return self::specialShuffle($pw);
}
/**
* multibyte-character safe shuffle function
*
* @param string $str
*
* @return string
*/
private static function specialShuffle(string $str): string
{
$len = mb_strlen($str);
$sploded = [];
while ($len-- > 0) {
$sploded[] = mb_substr($str, $len, 1);
}
shuffle($sploded);
return join('', $sploded);
}
/**
* return an array of available hashes
*
* @return array
*/
public static function getAvailablePasswordHashes(): array
{
// get available pwd-hases
$available_pwdhashes = [
PASSWORD_DEFAULT => lng('serversettings.systemdefault')
];
if (defined('PASSWORD_BCRYPT')) {
$available_pwdhashes[PASSWORD_BCRYPT] = 'Bcrypt/Blowfish' . (PASSWORD_DEFAULT == PASSWORD_BCRYPT ? ' (' . lng('serversettings.systemdefault') . ')' : '');
}
if (defined('PASSWORD_ARGON2I')) {
$available_pwdhashes[PASSWORD_ARGON2I] = 'Argon2i' . (PASSWORD_DEFAULT == PASSWORD_ARGON2I ? ' (' . lng('serversettings.systemdefault') . ')' : '');
}
if (defined('PASSWORD_ARGON2ID')) {
$available_pwdhashes[PASSWORD_ARGON2ID] = 'Argon2id' . (PASSWORD_DEFAULT == PASSWORD_ARGON2ID ? ' (' . lng('serversettings.systemdefault') . ')' : '');
}
return $available_pwdhashes;
}
/**
* Function validatePassword
*
* if password-min-length is set in settings
* we check against the length, if not matched
* an error message will be output and 'exit' is called
*
* @param string $password the password to validate
* @param bool $json_response
*
* @return string either the password or an errormessage+exit
*/
public static function validatePassword(string $password, bool $json_response = false): string
{
if (Settings::Get('panel.password_min_length') > 0) {
$password = Validate::validate($password, Settings::Get('panel.password_min_length'),
'/^.{' . (int)Settings::Get('panel.password_min_length') . ',}$/D', 'notrequiredpasswordlength', [],
$json_response);
}
if (Settings::Get('panel.password_regex') != '') {
$password = Validate::validate($password, Settings::Get('panel.password_regex'),
Settings::Get('panel.password_regex'), 'notrequiredpasswordcomplexity', [], $json_response);
} else {
if (Settings::Get('panel.password_alpha_lower')) {
$password = Validate::validate($password, '/.*[a-z]+.*/', '/.*[a-z]+.*/',
'notrequiredpasswordcomplexity', [], $json_response);
}
if (Settings::Get('panel.password_alpha_upper')) {
$password = Validate::validate($password, '/.*[A-Z]+.*/', '/.*[A-Z]+.*/',
'notrequiredpasswordcomplexity', [], $json_response);
}
if (Settings::Get('panel.password_numeric')) {
$password = Validate::validate($password, '/.*[0-9]+.*/', '/.*[0-9]+.*/',
'notrequiredpasswordcomplexity', [], $json_response);
}
if (Settings::Get('panel.password_special_char_required')) {
$password = Validate::validate($password,
'/.*[' . preg_quote(Settings::Get('panel.password_special_char'), '/') . ']+.*/',
'/.*[' . preg_quote(Settings::Get('panel.password_special_char'), '/') . ']+.*/',
'notrequiredpasswordcomplexity', [], $json_response);
}
}
return $password;
}
/**
* Function validatePasswordLogin
*
* compare user password-hash with given user-password
* and check if they are the same
* additionally it updates the hash if the system settings changed
* or if the very old md5() sum is used
*
* @param array $userinfo user-data from table
* @param string $password the password to validate
* @param string $table either panel_customers or panel_admins
* @param string $uid user-id-field in $table
*
* @return bool
* @throws \Exception
*/
public static function validatePasswordLogin(
array $userinfo,
string $password,
string $table = 'panel_customers',
string $uid = 'customerid'
): bool {
$algo = Settings::Get('system.passwordcryptfunc') !== null ? Settings::Get('system.passwordcryptfunc') : PASSWORD_DEFAULT;
if (is_numeric($algo)) {
// old setting format
$algo = PASSWORD_DEFAULT;
Settings::Set('system.passwordcryptfunc', $algo);
}
$pwd_hash = $userinfo['password'];
$update_hash = false;
$pwd_check = "";
// check for good'ole md5
if (strlen($pwd_hash) == 32 && ctype_xdigit($pwd_hash)) {
$pwd_check = md5($password);
$update_hash = true;
}
if ($pwd_hash === $pwd_check || password_verify($password, $pwd_hash)) {
// check for update of hash (only if our database is ready to handle the bigger string)
$is_ready = Froxlor::versionCompare2("0.9.33", Froxlor::getVersion()) <= 0;
if ((password_needs_rehash($pwd_hash, $algo) || $update_hash) && $is_ready) {
$upd_stmt = Database::prepare("
UPDATE " . $table . " SET `password` = :newpasswd WHERE `" . $uid . "` = :uid
");
$params = [
'newpasswd' => self::makeCryptPassword($password),
'uid' => $userinfo[$uid]
];
Database::pexecute($upd_stmt, $params);
}
return true;
}
return false;
}
/**
* Make encrypted password from clear text password
*
* @param string $password Password to be encrypted
* @param bool $htpasswd optional whether to generate a bcrypt password for directory protection
* @param bool $ftpd optional generates sha256 password strings for proftpd/pureftpd
*
* @return string encrypted password
*/
public static function makeCryptPassword(string $password, bool $htpasswd = false, bool $ftpd = false): string
{
if ($htpasswd || $ftpd) {
if ($ftpd) {
// sha256 compatible for proftpd and pure-ftpd
return crypt($password, '$5$' . self::generatePassword(16, true) . '$');
}
// bcrypt hash for dir-protection
return password_hash($password, PASSWORD_BCRYPT);
}
// crypt using the specified crypt-algorithm or system default
$algo = Settings::Get('system.passwordcryptfunc') !== null ? Settings::Get('system.passwordcryptfunc') : PASSWORD_DEFAULT;
return password_hash($password, $algo);
}
/**
* creates a self-signed ECC-certificate for the froxlor-vhost
* and sets the content to the corresponding files set in the
* settings for ssl-certificate-file and ssl-certificate-key
*
* @return void
*/
public static function createSelfSignedCertificate()
{
// validate that we have file names in the settings
$certFile = Settings::Get('system.ssl_cert_file');
$keyFile = Settings::Get('system.ssl_key_file');
if (empty($certFile)) {
$certFile = '/etc/ssl/froxlor_selfsigned.pem';
Settings::Set('system.ssl_cert_file', $certFile);
}
if (empty($keyFile)) {
$keyFile = '/etc/ssl/froxlor_selfsigned.key';
Settings::Set('system.ssl_key_file', $keyFile);
}
// certificate info
$dn = [
"countryName" => "DE",
"stateOrProvinceName" => "Hessen",
"localityName" => "Frankfurt am Main",
"organizationName" => "froxlor",
"organizationalUnitName" => "froxlor Server Management Panel",
"commonName" => Settings::Get('system.hostname'),
"emailAddress" => Settings::Get('panel.adminmail')
];
// create private key
$privkey = openssl_pkey_new([
"private_key_type" => OPENSSL_KEYTYPE_EC,
"curve_name" => 'prime256v1',
]);
// create signing request
$csr = openssl_csr_new($dn, $privkey, array('digest_alg' => 'sha384'));
// sign csr
$x509 = openssl_csr_sign($csr, null, $privkey, 365, array('digest_alg' => 'sha384'));
// export to files
openssl_x509_export_to_file($x509, $certFile);
openssl_pkey_export_to_file($privkey, $keyFile);
}
}