Skip to content

Commit

Permalink
Fix PasswordStrength unicode support
Browse files Browse the repository at this point in the history
This adds a new option `unicodeEquality` to thread `²` as number rather then a special character.
This option is false by default, for compatibility. But should be enabled for better security.
  • Loading branch information
sstok committed Apr 19, 2017
1 parent d30a940 commit 12881f3
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 27 deletions.
5 changes: 3 additions & 2 deletions docs/strength-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ constraint with the following options.
* message: The validation message (default: password_too_weak)
* minLength: Minimum length of the password, should be at least 6 (or 8 for better security)
* minStrength: Minimum required strength of the password.
* unicodeEquality: Consider characters from other scripts (unicode) as equal.
When set to false (default) `²` will seen as a special character rather then then 2 in another script.

The strength is computed from various measures including
length and usage of (special) characters.
The strength is computed from various measures including length and usage of (special) characters.

**Note:** A strength is measured by the presence of a character and total length.
One can have a 'medium' password consisting of only a-z and A-Z, but with a length higher than 12 characters.
Expand Down
1 change: 1 addition & 0 deletions src/Validator/Constraints/PasswordStrength.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class PasswordStrength extends Constraint
public $message = 'password_too_weak';
public $minLength = 6;
public $minStrength;
public $unicodeEquality = false;

/**
* {@inheritdoc}
Expand Down
94 changes: 69 additions & 25 deletions src/Validator/Constraints/PasswordStrengthValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ public function validate($password, Constraint $constraint)
}

$password = (string) $password;

$passwordStrength = 0;
$passLength = mb_strlen($password);

if ($passLength < $constraint->minLength) {
Expand All @@ -103,30 +101,10 @@ public function validate($password, Constraint $constraint)

$tips = array();

if (preg_match('/[a-zA-Z]/', $password)) {
++$passwordStrength;

if (!preg_match('/[a-z]/', $password)) {
$tips[] = 'lowercase_letters';
} elseif (preg_match('/[A-Z]/', $password)) {
++$passwordStrength;
} else {
$tips[] = 'uppercase_letters';
}
} else {
$tips[] = 'letters';
}

if (preg_match('/\d+/', $password)) {
++$passwordStrength;
if ($constraint->unicodeEquality) {
$passwordStrength = $this->calculateStrengthUnicode($password, $tips);
} else {
$tips[] = 'numbers';
}

if (preg_match('/[^a-zA-Z0-9]/', $password)) {
++$passwordStrength;
} else {
$tips[] = 'special_chars';
$passwordStrength = $this->calculateStrength($password, $tips);
}

if ($passLength > 12) {
Expand Down Expand Up @@ -162,4 +140,70 @@ public function translateTips($tip)
{
return $this->translator->trans('rollerworks_password.tip.'.$tip, array(), 'validators');
}

private function calculateStrength($password, &$tips)
{
$passwordStrength = 0;

if (preg_match('/[a-zA-Z]/', $password)) {
++$passwordStrength;

if (!preg_match('/[a-z]/', $password)) {
$tips[] = 'lowercase_letters';
} elseif (preg_match('/[A-Z]/', $password)) {
++$passwordStrength;
} else {
$tips[] = 'uppercase_letters';
}
} else {
$tips[] = 'letters';
}

if (preg_match('/\d+/', $password)) {
++$passwordStrength;
} else {
$tips[] = 'numbers';
}

if (preg_match('/[^a-zA-Z0-9]/', $password)) {
++$passwordStrength;
} else {
$tips[] = 'special_chars';
}

return $passwordStrength;
}

private function calculateStrengthUnicode($password, &$tips)
{
$passwordStrength = 0;

if (preg_match('/\p{L}/u', $password)) {
++$passwordStrength;

if (!preg_match('/\p{Ll}/u', $password)) {
$tips[] = 'lowercase_letters';
} elseif (preg_match('/\p{Lu}/u', $password)) {
++$passwordStrength;
} else {
$tips[] = 'uppercase_letters';
}
} else {
$tips[] = 'letters';
}

if (preg_match('/\p{N}/u', $password)) {
++$passwordStrength;
} else {
$tips[] = 'numbers';
}

if (preg_match('/[^\p{L}\p{N}]/u', $password)) {
++$passwordStrength;
} else {
$tips[] = 'special_chars';
}

return $passwordStrength;
}
}
55 changes: 55 additions & 0 deletions tests/Validator/PasswordStrengthTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,40 @@ public function getWeakPasswords()
);
}

public function getWeakPasswordsUnicode()
{
$pre = 'rollerworks_password.tip.';

// \u{FD3E} = ﴾ = Arabic ornate left parenthesis

return array(
// Very weak
array(2, 'weaker', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"),
array(2, '123456', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"),
array(2, '²²²²²²', 1, "{$pre}letters, {$pre}special_chars, {$pre}length"),
array(2, 'foobar', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"),
array(2, 'ömgwat', 1, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars, {$pre}length"),
array(2, '!.!.!.', 1, "{$pre}letters, {$pre}numbers, {$pre}length"),
array(2, '!.!.!﴾', 1, "{$pre}letters, {$pre}numbers, {$pre}length"),

// Weak
array(3, 'wee6eak', 2, "{$pre}uppercase_letters, {$pre}special_chars, {$pre}length"),
array(3, 'foobar!', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}length"),
array(3, 'Foobar', 2, "{$pre}numbers, {$pre}special_chars, {$pre}length"),
array(3, '123456!', 2, "{$pre}letters, {$pre}length"),
array(3, '7857375923752947', 2, "{$pre}letters, {$pre}special_chars"),
array(3, 'FSDFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"),
array(3, 'FÜKFJSLKFFSDFDSF', 2, "{$pre}lowercase_letters, {$pre}numbers, {$pre}special_chars"),
array(3, 'fjsfjdljfsjsjjlsj', 2, "{$pre}uppercase_letters, {$pre}numbers, {$pre}special_chars"),

// Medium
array(4, 'Foobar﴾', 3, "{$pre}numbers, {$pre}length"),
array(4, 'foo-b0r!', 3, "{$pre}uppercase_letters, {$pre}length"),
array(4, 'fjsfjdljfsjsjjls1', 3, "{$pre}uppercase_letters, {$pre}special_chars"),
array(4, '785737592375294b', 3, "{$pre}uppercase_letters, {$pre}special_chars"),
);
}

public static function getStrongPasswords()
{
return array(
Expand Down Expand Up @@ -179,6 +213,27 @@ public function testWeakPasswordsWillNotPass($minStrength, $value, $currentStren
->assertRaised();
}

/**
* @dataProvider getWeakPasswordsUnicode
*/
public function testWeakPasswordsWithUnicodeWillNotPass($minStrength, $value, $currentStrength, $tips = '')
{
$constraint = new PasswordStrength(array('minStrength' => $minStrength, 'minLength' => 6, 'unicodeEquality' => true));

$this->validator->validate($value, $constraint);

$parameters = array(
'{{ length }}' => 6,
'{{ min_strength }}' => 'rollerworks_password.strength_level.'.self::$levelToLabel[$minStrength],
'{{ current_strength }}' => 'rollerworks_password.strength_level.'.self::$levelToLabel[$currentStrength],
'{{ strength_tips }}' => $tips,
);

$this->buildViolation('password_too_weak')
->setParameters($parameters)
->assertRaised();
}

/**
* @dataProvider getVeryStrongPasswords
*/
Expand Down

0 comments on commit 12881f3

Please sign in to comment.