Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

36 add password policy #39

Merged
merged 1 commit into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"ext-ldap": ">=7.4",
"phpmailer/phpmailer": "^6.5.0",
"symfony/cache": "^v5.4.42",
"predis/predis": "^v2.2.2"
"predis/predis": "^v2.2.2",
"bjeavons/zxcvbn-php": "^1.0",
"mxrxdxn/pwned-passwords": "^v2.1.0"
},
"require-dev": {
"phpunit/phpunit": ">=8",
Expand Down
271 changes: 271 additions & 0 deletions src/Ltb/Ppolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
<?php namespace Ltb;

use ZxcvbnPhp\Zxcvbn;
use PwnedPasswords\PwnedPasswords;

/**
* Password functions
*/
final class Ppolicy {

# Check password strength
# @param string password to check
# @param string old password
# @param array password policy configuration
# @param string user identifier
# @param array ldap entry
# @param array of key/values : configuration of custom password fields to check
# @return result code
static function check_password_strength( $password,
$oldpassword,
$pwd_policy_config,
$login,
$entry_array,
$change_custompwdfield )
{
extract( $pwd_policy_config );

$result = "";

$length = mb_strlen($password, mb_detect_encoding($password));
preg_match_all("/[a-z]/", $password, $lower_res);
$lower = count( $lower_res[0] );
preg_match_all("/[A-Z]/", $password, $upper_res);
$upper = count( $upper_res[0] );
preg_match_all("/[0-9]/", $password, $digit_res);
$digit = count( $digit_res[0] );

$special = 0;
$special_at_ends = false;
if ( isset($pwd_special_chars) && !empty($pwd_special_chars) ) {
preg_match_all("/[$pwd_special_chars]/", $password, $special_res);
$special = count( $special_res[0] );
if ( $pwd_no_special_at_ends ) {
$special_at_ends = preg_match(
"/(^[$pwd_special_chars]|[$pwd_special_chars]$)/",
$password,
$special_res
);
}
}

$forbidden = 0;
if ( isset($pwd_forbidden_chars) && !empty($pwd_forbidden_chars) ) {
preg_match_all("/[$pwd_forbidden_chars]/", $password, $forbidden_res);
$forbidden = count( $forbidden_res[0] );
}

# Complexity: checks for lower, upper, special, digits
if ( $pwd_complexity ) {
$complex = 0;
if ( $special > 0 ) { $complex++; }
if ( $digit > 0 ) { $complex++; }
if ( $lower > 0 ) { $complex++; }
if ( $upper > 0 ) { $complex++; }
if ( $complex < $pwd_complexity ) { $result="notcomplex"; }
}

# Minimal length
if ( $pwd_min_length and $length < $pwd_min_length ) { $result="tooshort"; }

# Maximal length
if ( $pwd_max_length and $length > $pwd_max_length ) { $result="toobig"; }

# Minimal lower chars
if ( $pwd_min_lower and $lower < $pwd_min_lower ) { $result="minlower"; }

# Minimal upper chars
if ( $pwd_min_upper and $upper < $pwd_min_upper ) { $result="minupper"; }

# Minimal digit chars
if ( $pwd_min_digit and $digit < $pwd_min_digit ) { $result="mindigit"; }

# Minimal special chars
if ( $pwd_min_special and $special < $pwd_min_special ) { $result="minspecial"; }

# Forbidden chars
if ( $forbidden > 0 ) { $result="forbiddenchars"; }

# Special chars at beginning or end
if ( $special_at_ends > 0 && $special == 1 ) { $result="specialatends"; }

# Same as old password?
if ( $pwd_no_reuse and $password === $oldpassword ) { $result="sameasold"; }

# Same as login?
if ( $pwd_diff_login and $password === $login ) { $result="sameaslogin"; }

if ( $pwd_diff_last_min_chars > 0 and strlen($oldpassword) > 0 ) {
$similarities = similar_text($oldpassword, $password);
$check_len = strlen($oldpassword) < strlen($password) ?
strlen($oldpassword) :
strlen($password);
$new_chars = $check_len - $similarities;
if ($new_chars <= $pwd_diff_last_min_chars) { $result = "diffminchars"; }
}

# Contains forbidden words?
if ( !empty($pwd_forbidden_words) ) {
foreach( $pwd_forbidden_words as $disallowed ) {
if( stripos($password, $disallowed) !== false ) {
$result="forbiddenwords";
break;
}
}
}

# Contains values from forbidden ldap fields?
if ( !empty($pwd_forbidden_ldap_fields) ) {
foreach ( $pwd_forbidden_ldap_fields as $field ) {
# if entry does not hold requested attribute, continue
if ( array_key_exists($field,$entry_array) )
{
$values = $entry_array[$field];
if (!is_array($values)) {
$values = array($values);
}
foreach ($values as $key => $value) {
if ($key === 'count') {
continue;
}
if (stripos($password, $value) !== false) {
$result = "forbiddenldapfields";
break 2;
}
}
}
}
}

# ensure that the new password is different from any other custom password field marked as unique
foreach ( $change_custompwdfield as $custompwdfield) {
if (isset($custompwdfield['pwd_policy_config']['pwd_unique_across_custom_password_fields']) &&
$custompwdfield['pwd_policy_config']['pwd_unique_across_custom_password_fields']) {
if (array_key_exists($custompwdfield['attribute'], $entry_array)) {
if ($custompwdfield['hash'] == 'auto') {
$matches = [];
if ( preg_match( '/^\{(\w+)\}/',
$entry_array[$custompwdfield['attribute']][0],
$matches ) )
{
$hash_for_custom_pwd = strtoupper($matches[1]);
}
} else {
$hash_for_custom_pwd = $custompwdfield['hash'];
}
if ( \Ltb\Password::check_password($password,
$entry_array[$custompwdfield['attribute']][0],
$hash_for_custom_pwd) )
{
$result = "sameascustompwd";
}
}
}
}

# pwned?
if ($use_pwnedpasswords and version_compare(PHP_VERSION, '7.2.5') >= 0) {
$pwned_passwords = new PwnedPasswords;
$insecure = $pwned_passwords->isPwned($password);
if ($insecure) { $result="pwned"; }
}


# check entropy
$zxcvbn = new Zxcvbn();
if( isset($pwd_check_entropy) && $pwd_check_entropy == true )
{
if( isset($pwd_min_entropy) && is_int($pwd_min_entropy) )
{
// force encoding to utf8, as iso-8859-1 is not supported by zxcvbn
//$password = mb_convert_encoding($p, 'UTF-8', 'ISO-8859-1');
error_log("checkEntropy: password taken directly");
$entropy = $zxcvbn->passwordStrength("$password");
$entropy_level = intval($entropy["score"]);
$entropy_message = $entropy['feedback']['warning'] ? strval($entropy['feedback']['warning']) : "";
error_log( "checkEntropy: level $entropy_level msg: $entropy_message" );
if( is_int($entropy_level) && $entropy_level >= $pwd_min_entropy )
{
; // password entropy check ok
}
else
{
error_log("checkEntropy: insufficient entropy: level = $entropy_level but minimal required = $pwd_min_entropy");
$result="insufficiententropy";
}
}
else
{
error_log("checkEntropy: missing required parameter pwd_min_entropy");
$result="insufficiententropy";
}

}

return $result;
}

/* Check user password against zxcvbn library
Input : new user base64-encoded password
Output: JSON response: { "level" => int, "message" => "msg" } */

static function checkEntropyJSON($password_base64)
{
$response_params = array();
$zxcvbn = new Zxcvbn();

if( ! isset($password_base64) || empty($password_base64))
{
error_log("checkEntropy: missing parameter password");
$response_params["level"] = "-1";
$response_params["message"] = "missing parameter password";
return json_encode($response_params);
}

$p = base64_decode($password_base64);
// force encoding to utf8, as iso-8859-1 is not supported by zxcvbn
$password = mb_convert_encoding($p, 'UTF-8', 'ISO-8859-1');

$entropy = $zxcvbn->passwordStrength("$password");

$response_params["level"] = strval($entropy["score"]);
$response_params["message"] = $entropy['feedback']['warning'] ? strval($entropy['feedback']['warning']) : "";

return json_encode($response_params);
}

static function smarty_assign_variable($smarty, $pwd_policy_config)
{
foreach ($pwd_policy_config as $param => $value) {
if( isset($value) )
{
// only send password policy parameters
// of type string to smarty template
if( !is_array($value) )
{
$smarty->assign($param, $value);
}
}
}
}

static function smarty_assign_ppolicy($smarty, $pwd_show_policy_pos, $pwd_show_policy, $result, $pwd_policy_config )
{
if (isset($pwd_show_policy_pos)) {
$smarty->assign('pwd_show_policy_pos', $pwd_show_policy_pos);
$smarty->assign('pwd_show_policy', $pwd_show_policy);
$smarty->assign('pwd_show_policy_onerror', true);
if ( $pwd_show_policy === "onerror" ) {
if ( !preg_match( "/tooshort|toobig|minlower|minupper|mindigit|minspecial|forbiddenchars|sameasold|notcomplex|sameaslogin|pwned|specialatends/" , $result) ) {
$smarty->assign('pwd_show_policy_onerror', false);
} else {
$smarty->assign('pwd_show_policy_onerror', true);
}
}
self::smarty_assign_variable($smarty, $pwd_policy_config);

// send policy to a JSON object usable in javascript
$smarty->assign('json_policy', base64_encode(json_encode( $pwd_policy_config )));
}
}
}
33 changes: 33 additions & 0 deletions src/ppolicy/css/ppolicy.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* password entropy customization*/
#entropybar>div {
width: 0%;
/* Adjust with JavaScript */
}

#entropybar>div.levelErr {
width: 0%;
}

#entropybar>div.level0 {
width: 20%;
}

#entropybar>div.level1 {
width: 40%;
}

#entropybar>div.level2 {
width: 60%;
}

#entropybar>div.level3 {
width: 80%;
}

#entropybar>div.level4 {
width: 100%;
}

.entropyHidden {
display: none;
}
Loading
Loading