diff --git a/plugins/change-password/drivers/example.phps b/plugins/change-password/drivers/example.phps new file mode 100644 index 000000000..552861a4d --- /dev/null +++ b/plugins/change-password/drivers/example.phps @@ -0,0 +1,37 @@ +oLogger = $oLogger; +// $this->sHostName = $oConfig->Get('plugin', 'example_hostname', ''); + } + + public static function isSupported() : bool + { + return false; + } + + public static function configMapping() : array + { + return array(); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool + { + return false; + } +} diff --git a/plugins/change-password/drivers/pdo.php b/plugins/change-password/drivers/pdo.php new file mode 100644 index 000000000..0a18f2d08 --- /dev/null +++ b/plugins/change-password/drivers/pdo.php @@ -0,0 +1,94 @@ +oLogger = $oLogger; + $this->dsn = $oConfig->Get('plugin', 'pdo_dsn', ''); + $this->user = $oConfig->Get('plugin', 'pdo_user', ''); + $this->pass = $oConfig->Get('plugin', 'pdo_password', ''); + $this->sql = $oConfig->Get('plugin', 'pdo_sql', ''); + $this->encrypt = $oConfig->Get('plugin', 'pdo_encrypt', ''); + $this->encrypt_prefix = $oConfig->Get('plugin', 'pdo_encryptprefix', ''); + } + + public static function isSupported() : bool + { + return true; + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('pdo_dsn')->SetLabel('DSN') + ->SetDefaultValue('mysql:host=localhost;dbname=snappymail;charset=utf8'), + \RainLoop\Plugins\Property::NewInstance('pdo_user')->SetLabel('User'), + \RainLoop\Plugins\Property::NewInstance('pdo_password')->SetLabel('Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD), + \RainLoop\Plugins\Property::NewInstance('pdo_sql')->SetLabel('Statement') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('SQL statement (allowed wildcards :email, :oldpass, :newpass, :domain, :username).') + ->SetDefaultValue('UPDATE table SET password = :newpass WHERE domain = :domain AND username = :username and oldpass = :oldpass'), + \RainLoop\Plugins\Property::NewInstance('pdo_encrypt')->SetLabel('Encryption') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue(array('none', 'bcrypt', 'Argon2i', 'Argon2id', 'SHA256-CRYPT', 'SHA512-CRYPT')) + ->SetDescription('In what way do you want the passwords to be encrypted?'), + \RainLoop\Plugins\Property::NewInstance('pdo_encryptprefix')->SetLabel('Encrypt prefix') + ->SetDescription('Optional encrypted password prefix, like: {BLF-CRYPT}'), + ); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool + { + $options = array( + \PDO::ATTR_EMULATE_PREPARES => true, + \PDO::ATTR_PERSISTENT => true, + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION + ); + + $conn = new \PDO($this->dsn, $this->user, $this->pass, $options); + + //prepare SQL varaibles + $sEmail = $oAccount->Email(); + $sEmailUser = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail); + $sEmailDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail); + + $placeholders = array( + ':email' => $sEmail, + ':oldpass' => $this->encrypt_prefix . \ChangePasswordPlugin::encrypt($this->encrypt, $sPrevPassword), + ':newpass' => $this->encrypt_prefix . \ChangePasswordPlugin::encrypt($this->encrypt, $sNewPassword), + ':domain' => $sEmailDomain, + ':username' => $sEmailUser + ); + + $statement = $conn->prepare($this->sql); + + // we have to check that all placehoders are used in the query, passing any unused placeholders will generate an error + foreach ($placeholders as $placeholder => $value) { + if (\preg_match_all("/{$placeholder}(?![a-zA-Z0-9\-])/", $this->sql)) { + $statement->bindValue($placeholder, $value); + } + } + + // and execute + return !!$statement->execute(); + } +} diff --git a/plugins/change-password/index.php b/plugins/change-password/index.php index ef66009c1..ca2605bd4 100644 --- a/plugins/change-password/index.php +++ b/plugins/change-password/index.php @@ -22,29 +22,43 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addJs('js/ChangePasswordUserSettings.js'); // add js file $this->addJsonHook('ChangePassword', 'ChangePassword'); $this->addTemplate('templates/SettingsChangePassword.html'); + } - /** - * Admin - */ -/* - $this->addJs('js/ChangePasswordAdminSettings.js', true); // add js file - $this->addJsonHook('AdminChangePassword', 'AdminChangePassword'); - $this->addTemplate('templates/ChangePasswordAdminSettings.html', true); -*/ + public function configMapping() : array + { + $result = []; + foreach (\glob(__DIR__ . '/drivers/*.php') as $file) { + require_once $file; + $name = \basename($file, '.php'); + $class = 'ChangePasswordDriver' . $name; + if ($class::isSupported()) { + $result[] = \RainLoop\Plugins\Property::NewInstance("driver_{$name}") + ->SetLabel('Enable ' . $class::NAME) + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription($class::DESCRIPTION); + $result[] = \RainLoop\Plugins\Property::NewInstance("driver_{$name}_allowed_emails") + ->SetLabel('Allowed emails') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net') + ->SetDefaultValue('*'); + $result = \array_merge($result, \call_user_func("{$class}::configMapping")); + } + } + return $result; } public function ChangePassword() { + if (!$oAccount->Email()) { + throw new ClientException(static::CouldNotSaveNewPassword); + } + $sPrevPassword = $this->jsonParam('PrevPassword'); $sNewPassword = $this->jsonParam('NewPassword'); $oActions = $this->Manager()->Actions(); $oAccount = $oActions->GetAccount(); -/* - if (!$this->isPossible($oAccount)) { - throw new ClientException(static::CouldNotSaveNewPassword); - } -*/ + if ($sPrevPassword !== $oAccount->Password()) { throw new ClientException(static::CurrentPasswordIncorrect, null, $oActions->StaticI18N('NOTIFICATIONS/CURRENT_PASSWORD_INCORRECT')); } @@ -57,23 +71,77 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin if (!\MailSo\Base\Utils::PasswordWeaknessCheck($sPasswordForCheck)) { throw new ClientException(static::NewPasswordWeak, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_WEAK')); } -/* - require __DIR__ . '/drivers/pdo.php'; - $this->oDriver = new \ChangePasswordDriverPDO; - if (!$this->oDriver->ChangePassword($oAccount, $sPrevPassword, $sNewPassword)) { - throw new ClientException(static::CouldNotSaveNewPassword); + $bResult = false; + $oConfig = $this->Config(); + foreach (\glob(__DIR__ . '/drivers/*.php') as $file) { + $name = \basename($file, '.php'); + if ($oConfig->Get('plugin', "driver_{$name}", false) + && \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $oConfig->Get('plugin', "driver_{$name}_allowed_emails")) + ) { + require_once $file; + $class = 'ChangePasswordDriver' . $name; + $name = $class::NAME; + try + { + if ($class::isSupported()) { + $oDriver = new $class( + $oConfig(), + $oActions->Logger() + ); + if (!$oDriver->ChangePassword($oAccount, $sPrevPassword, $sNewPassword)) { + throw new ClientException(static::CouldNotSaveNewPassword); + } + $bResult = true; + if ($this->oLogger) { + $this->oLogger->Write("{$name} password changed for {$oAccount->Email()}"); + } + } + } + catch (\Throwable $oException) + { + if ($this->oLogger) { + $this->oLogger->Write("ERROR: {$name} password change for {$oAccount->Email()} failed"); + $this->oLogger->WriteException($oException); +// $this->oLogger->WriteException($oException, \MailSo\Log\Enumerations\Type::WARNING, $name); + } + } + } + } + + if ($bResult) { + $oAccount->SetPassword($sNewPassword); + $oActions->SetAuthToken($oAccount); } - $oAccount->SetPassword($sNewPassword); - $oActions->SetAuthToken($oAccount); -*/ return $oActions->GetSpecAuthToken(); // return $this->jsonResponse(__FUNCTION__, $oActions->GetSpecAuthToken()); } - public function AdminChangePassword() + public static function encrypt(string $algo, string $password) { + switch (\strtolower($algo)) + { + case 'argon2i': + return \password_hash($password, PASSWORD_ARGON2I); + + case 'argon2id': + return \password_hash($password, PASSWORD_ARGON2ID); + + case 'bcrypt': + return \password_hash($password, PASSWORD_BCRYPT); + + case 'sha256-crypt': + return \crypt($password,'$5$'.\substr(\base64_encode(\random_bytes(32)), 0, 16)); + + case 'sha512-crypt': + return \crypt($password,'$6$'.\substr(\base64_encode(\random_bytes(32)), 0, 16)); + + default: + break; + } + + return $sPassword; } }