password strength special characters

This commit is contained in:
Yuri Kuznetsov
2024-11-19 14:48:55 +02:00
parent 8fc02ce0f7
commit f0fb1725ad
15 changed files with 356 additions and 68 deletions

View File

@@ -29,30 +29,18 @@
namespace Espo\Core\Formula\Functions\PasswordGroup;
use Espo\Core\Formula\{
Functions\BaseFunction,
ArgumentList,
};
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Func;
use Espo\Tools\UserSecurity\Password\Generator;
use Espo\Core\Utils\Util;
use Espo\Core\Di;
class GenerateType extends BaseFunction implements
Di\ConfigAware
class GenerateType implements Func
{
use Di\ConfigSetter;
public function __construct(
private Generator $generator,
) {}
public function process(ArgumentList $args)
public function process(EvaluatedArgumentList $arguments): string
{
$config = $this->config;
$length = $config->get('passwordGenerateLength', 10);
$letterCount = $config->get('passwordGenerateLetterCount', 4);
$numberCount = $config->get('passwordGenerateNumberCount', 2);
$password = Util::generatePassword($length, $letterCount, $numberCount, true);
return $password;
return $this->generator->generate();
}
}

View File

@@ -983,13 +983,13 @@ class Util
* @param int $letters A number of letters.
* @param int $digits A number of digits.
* @param bool $bothCases Use upper and lower case letters.
* @return string
*/
public static function generatePassword(
int $length = 8,
int $letters = 5,
int $digits = 3,
bool $bothCases = false
bool $bothCases = false,
int $specialCharacters = 0
): string {
$chars = [
@@ -998,6 +998,7 @@ class Util
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'abcdefghijklmnopqrstuvwxyz',
"'-!\"#$%&()*,./:;?@[]^_`{|}~+<=>",
];
$shuffle = function ($array) {
@@ -1030,7 +1031,7 @@ class Util
}
}
$either = $length - ($letters + $digits + $upperCase + $lowerCase);
$either = $length - ($letters + $digits + $upperCase + $lowerCase + $specialCharacters);
if ($either < 0) {
$either = 0;
@@ -1038,7 +1039,7 @@ class Util
$array = [];
foreach ([$letters, $digits, $either, $upperCase, $lowerCase] as $i => $len) {
foreach ([$letters, $digits, $either, $upperCase, $lowerCase, $specialCharacters] as $i => $len) {
$set = $chars[$i];
$subArray = [];

View File

@@ -34,19 +34,19 @@ use Espo\Entities\PasswordChangeRequest;
use Espo\Core\Utils\Client\ActionRenderer;
use Espo\Core\EntryPoint\EntryPoint;
use Espo\Core\EntryPoint\Traits\NoAuth;
use Espo\Core\Utils\Config;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\ORM\EntityManager;
use Espo\Tools\UserSecurity\Password\ConfigProvider;
class ChangePassword implements EntryPoint
{
use NoAuth;
public function __construct(
private Config $config,
private EntityManager $entityManager,
private ActionRenderer $actionRenderer
private ActionRenderer $actionRenderer,
private ConfigProvider $configProvider,
) {}
public function run(Request $request, Response $response): void
@@ -65,13 +65,14 @@ class ChangePassword implements EntryPoint
->findOne();
$strengthParams = [
'passwordGenerateLength' => $this->config->get('passwordGenerateLength'),
'passwordGenerateLetterCount' => $this->config->get('passwordGenerateLetterCount'),
'generateNumberCount' => $this->config->get('generateNumberCount'),
'passwordStrengthLength' => $this->config->get('passwordStrengthLength'),
'passwordStrengthLetterCount' => $this->config->get('passwordStrengthLetterCount'),
'passwordStrengthNumberCount' => $this->config->get('passwordStrengthNumberCount'),
'passwordStrengthBothCases' => $this->config->get('passwordStrengthBothCases'),
'passwordGenerateLength' => $this->configProvider->getGenerateLength(),
'passwordGenerateLetterCount' => $this->configProvider->getGenerateLetterCount(),
'generateNumberCount' => $this->configProvider->getGenerateNumberCount(),
'passwordStrengthLength' => $this->configProvider->getStrengthLength(),
'passwordStrengthLetterCount' => $this->configProvider->getStrengthLetterCount(),
'passwordStrengthNumberCount' => $this->configProvider->getStrengthNumberCount(),
'passwordStrengthBothCases' => $this->configProvider->getStrengthBothCases(),
'passwordStrengthSpecialCharacterCount' => $this->configProvider->getStrengthSpecialCharacterCount(),
];
$options = [

View File

@@ -280,6 +280,11 @@ return [
'ldapUserObjectClass' => 'person',
'ldapPortalUserLdapAuth' => false,
'passwordGenerateLength' => 10,
'passwordStrengthLength' => null,
'passwordStrengthLetterCount' => null,
'passwordStrengthNumberCount' => null,
'passwordStrengthBothCases' => false,
'passwordStrengthSpecialCharacterCount' => null,
'massActionIdleCountThreshold' => 100,
'exportIdleCountThreshold' => 1000,
'clientSecurityHeadersDisabled' => false,

View File

@@ -146,6 +146,7 @@
"passwordStrengthLetterCount": "Number of letters required in password",
"passwordStrengthNumberCount": "Number of digits required in password",
"passwordStrengthBothCases": "Password must contain letters of both upper and lower case",
"passwordStrengthSpecialCharacterCount": "Number of special character required in password",
"auth2FA": "Enable 2-Factor Authentication",
"auth2FAForced": "Force regular users to set up 2FA",
"auth2FAMethodList": "Available 2FA methods",

View File

@@ -107,6 +107,7 @@
"passwordStrengthLength": "Must be at least {length} characters long.",
"passwordStrengthLetterCount": "Must contain at least {count} letter(s).",
"passwordStrengthNumberCount": "Must contain at least {count} digit(s).",
"passwordStrengthSpecialCharacterCount": "Must contain at least {count} special character(s).",
"passwordStrengthBothCases": "Must contain letters of both upper and lower case.",
"passwordWillBeSent": "Password will be sent to user's email address.",
"passwordChanged": "Password has been changed",

View File

@@ -26,9 +26,11 @@
"tabLabel": "$label:Passwords",
"label": "Strength",
"rows": [
[{"name": "passwordGenerateLength"}, false],
[{"name": "passwordStrengthLength"}, {"name": "passwordStrengthLetterCount"}],
[{"name": "passwordStrengthBothCases"}, {"name": "passwordStrengthNumberCount"}]
[{"name": "passwordGenerateLength"}, {"name": "passwordStrengthLength"}],
[false, {"name": "passwordStrengthLetterCount"}],
[false, {"name": "passwordStrengthBothCases"}],
[false, {"name": "passwordStrengthNumberCount"}],
[false, {"name": "passwordStrengthSpecialCharacterCount"}]
]
},
{

View File

@@ -332,6 +332,11 @@
"max": 150,
"min": 0
},
"passwordStrengthSpecialCharacterCount": {
"type": "int",
"max": 50,
"min": 0
},
"passwordStrengthBothCases": {
"type": "bool"
},

View File

@@ -29,21 +29,17 @@
namespace Espo\Tools\UserSecurity\Password;
use Espo\Core\Utils\Config;
class Checker
{
private Config $config;
private const SPECIAL_CHARACTERS = "'-!\"#$%&()*,./:;?@[]^_`{|}~+<=>";
public function __construct(
Config $config
) {
$this->config = $config;
}
private ConfigProvider $configProvider,
) {}
public function checkStrength(string $password): bool
{
$minLength = $this->config->get('passwordStrengthLength');
$minLength = $this->configProvider->getStrengthLength();
if ($minLength) {
if (mb_strlen($password) < $minLength) {
@@ -51,7 +47,7 @@ class Checker
}
}
$requiredLetterCount = $this->config->get('passwordStrengthLetterCount');
$requiredLetterCount = $this->configProvider->getStrengthLetterCount();
if ($requiredLetterCount) {
$letterCount = 0;
@@ -67,7 +63,7 @@ class Checker
}
}
$requiredNumberCount = $this->config->get('passwordStrengthNumberCount');
$requiredNumberCount = $this->configProvider->getStrengthNumberCount();
if ($requiredNumberCount) {
$numberCount = 0;
@@ -83,7 +79,7 @@ class Checker
}
}
$bothCases = $this->config->get('passwordStrengthBothCases');
$bothCases = $this->configProvider->getStrengthBothCases();
if ($bothCases) {
$ucCount = 0;
@@ -103,6 +99,22 @@ class Checker
}
}
$specialCharacterCount = $this->configProvider->getStrengthSpecialCharacterCount();
if ($specialCharacterCount) {
$realSpecialCharacterCount = 0;
foreach (str_split($password) as $c) {
if (str_contains(self::SPECIAL_CHARACTERS, $c)) {
$realSpecialCharacterCount++;
}
}
if ($realSpecialCharacterCount < $specialCharacterCount) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,79 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\UserSecurity\Password;
use Espo\Core\Utils\Config;
class ConfigProvider
{
public function __construct(
private Config $config,
) {}
public function getStrengthLength(): ?int
{
return $this->config->get('passwordStrengthLength');
}
public function getStrengthLetterCount(): ?int
{
return $this->config->get('passwordStrengthLetterCount');
}
public function getStrengthNumberCount(): ?int
{
return $this->config->get('passwordStrengthNumberCount');
}
public function getStrengthSpecialCharacterCount(): ?int
{
return $this->config->get('passwordStrengthSpecialCharacterCount');
}
public function getStrengthBothCases(): bool
{
return (bool) $this->config->get('passwordStrengthBothCases');
}
public function getGenerateLength(): ?int
{
return $this->config->get('passwordGenerateLength');
}
public function getGenerateLetterCount(): ?int
{
return $this->config->get('passwordGenerateLetterCount');
}
public function getGenerateNumberCount(): ?int
{
return $this->config->get('passwordGenerateNumberCount');
}
}

View File

@@ -29,7 +29,6 @@
namespace Espo\Tools\UserSecurity\Password;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Util;
/**
@@ -39,30 +38,27 @@ use Espo\Core\Utils\Util;
*/
class Generator
{
private Config $config;
public function __construct(
Config $config
) {
$this->config = $config;
}
private ConfigProvider $configProvider,
) {}
/**
* Generate a password.
*/
public function generate(): string
{
$length = $this->config->get('passwordStrengthLength');
$letterCount = $this->config->get('passwordStrengthLetterCount');
$numberCount = $this->config->get('passwordStrengthNumberCount');
$length = $this->configProvider->getStrengthLength();
$letterCount = $this->configProvider->getStrengthLetterCount();
$numberCount = $this->configProvider->getStrengthNumberCount();
$specialCharacterCount = $this->configProvider->getStrengthSpecialCharacterCount() ?? 0;
$generateLength = $this->config->get('passwordGenerateLength', 10);
$generateLetterCount = $this->config->get('passwordGenerateLetterCount', 4);
$generateNumberCount = $this->config->get('passwordGenerateNumberCount', 2);
$generateLength = $this->configProvider->getGenerateLength() ?? 10;
$generateLetterCount = $this->configProvider->getGenerateLetterCount() ?? 4;
$generateNumberCount = $this->configProvider->getGenerateNumberCount() ?? 2;
$length = is_null($length) ? $generateLength : $length;
$letterCount = is_null($letterCount) ? $generateLetterCount : $letterCount;
$numberCount = is_null($letterCount) ? $generateNumberCount : $numberCount;
$numberCount = is_null($numberCount) ? $generateNumberCount : $numberCount;
if ($length < $generateLength) {
$length = $generateLength;
@@ -76,6 +72,6 @@ class Generator
$numberCount = $generateNumberCount;
}
return Util::generatePassword($length, $letterCount, $numberCount, true);
return Util::generatePassword($length, $letterCount, $numberCount, true, $specialCharacterCount);
}
}