diff --git a/composer.json b/composer.json
index 0580651..394d4e5 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,9 @@
"php": ">=7.4",
"ext-ldap": ">=7.4",
"phpmailer/phpmailer": "^6.5.0",
- "symfony/cache": "^v5.4.42"
+ "symfony/cache": "^v5.4.42",
+ "bjeavons/zxcvbn-php": "^1.0",
+ "mxrxdxn/pwned-passwords": "^v2.1.0"
},
"require-dev": {
"phpunit/phpunit": ">=8",
diff --git a/src/Ltb/Ppolicy.php b/src/Ltb/Ppolicy.php
new file mode 100644
index 0000000..48cf55e
--- /dev/null
+++ b/src/Ltb/Ppolicy.php
@@ -0,0 +1,271 @@
+ 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 )));
+ }
+ }
+}
diff --git a/src/ppolicy/css/ppolicy.css b/src/ppolicy/css/ppolicy.css
new file mode 100644
index 0000000..89f3332
--- /dev/null
+++ b/src/ppolicy/css/ppolicy.css
@@ -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;
+}
diff --git a/src/ppolicy/html/policy.tpl b/src/ppolicy/html/policy.tpl
new file mode 100644
index 0000000..d1a8e3a
--- /dev/null
+++ b/src/ppolicy/html/policy.tpl
@@ -0,0 +1,130 @@
+{if $pwd_show_policy === "onerror" and !$pwd_show_policy_onerror }
+{else}
+
+ {$msg_policy|unescape: "html" nofilter}
+
+
+
+ {if $pwd_min_length }
+ -
+
+ {$msg_policyminlength|unescape: "html" nofilter} {$pwd_min_length}
+
+ {/if}
+
+
+ {if $pwd_max_length }
+ -
+
+ {$msg_policymaxlength|unescape: "html" nofilter} {$pwd_max_length}
+
+ {/if}
+
+
+ {if $pwd_min_lower }
+ -
+
+ {$msg_policyminlower|unescape: "html" nofilter} {$pwd_min_lower}
+
+ {/if}
+
+
+ {if $pwd_min_upper }
+ -
+
+ {$msg_policyminupper|unescape: "html" nofilter} {$pwd_min_upper}
+
+ {/if}
+
+
+ {if $pwd_min_digit }
+ -
+
+ {$msg_policymindigit|unescape: "html" nofilter} {$pwd_min_digit}
+
+ {/if}
+
+
+ {if $pwd_min_special }
+ -
+
+ {$msg_policyminspecial|unescape: "html" nofilter} {$pwd_min_special}
+
+ {/if}
+
+
+ {if $pwd_complexity }
+ -
+
+ {$msg_policycomplex|unescape: "html" nofilter} {$pwd_complexity}
+
+ {/if}
+
+
+ {if $pwd_forbidden_chars }
+ -
+
+ {$msg_policyforbiddenchars|unescape: "html" nofilter} {$pwd_forbidden_chars}
+
+ {/if}
+
+
+ {if $pwd_diff_last_min_chars }
+ -
+
+ {$msg_policydiffminchars|unescape: "html" nofilter} {$pwd_diff_last_min_chars}
+
+ {/if}
+
+
+ {if $pwd_no_reuse }
+ -
+
+ {$msg_policynoreuse|unescape: "html" nofilter}
+
+ {/if}
+
+
+ {if $pwd_diff_login }
+ -
+
+ {$msg_policydifflogin|unescape: "html" nofilter}
+
+ {/if}
+
+
+ {if $use_pwnedpasswords }
+ -
+
+ {$msg_policypwned|unescape: "html" nofilter}
+
+ {/if}
+
+
+ {if $pwd_no_special_at_ends }
+ -
+
+ {$msg_policyspecialatends|unescape: "html" nofilter}
+
+ {/if}
+
+
+ {if $pwd_display_entropy }
+ -
+
+ {if $pwd_check_entropy }
+ {$msg_policyentropy|unescape: "html" nofilter}
+ {else}
+ {$msg_policyentropy|unescape: "html" nofilter}
+ {/if}
+
+
+
+ {/if}
+
+
+
+{/if}
+
diff --git a/src/ppolicy/js/ppolicy.js b/src/ppolicy/js/ppolicy.js
new file mode 100644
index 0000000..14af947
--- /dev/null
+++ b/src/ppolicy/js/ppolicy.js
@@ -0,0 +1,388 @@
+(function() {
+ var barWidth, bootstrapClasses, displayEntropyBar, displayEntropyBarMsg, ppolicyResults;
+
+ ppolicyResults = {};
+
+ ltbComponent = $("#ltb-component").text();
+ entropyPath = new Map([["ssp", "?action=checkentropy"], ["sd", "?page=checkentropy"]]);
+ loginField = new Map([["ssp", "#login"], ["sd", "#info_identifier td"]]);
+
+ bootstrapClasses = new Map([["Err", "bg-danger"], ["0", "bg-danger"], ["1", "bg-warning"], ["2", "bg-info"], ["3", "bg-primary"], ["4", "bg-success"]]);
+
+ barWidth = new Map([["Err", "0"], ["0", "20"], ["1", "40"], ["2", "60"], ["3", "80"], ["4", "100"]]);
+
+ json_policy = $("#json-policy").data('policy');
+ var local_policy = JSON.parse(atob(json_policy));
+
+ displayEntropyBar = function(level) {
+ $("#entropybar div").removeClass();
+ $("#entropybar div").addClass('progress-bar');
+ $("#entropybar div").width(barWidth.get(level) + '%');
+ $("#entropybar div").addClass(bootstrapClasses.get(level));
+ return $("#entropybar div").html(barWidth.get(level) + '%');
+ };
+
+ displayEntropyBarMsg = function(msg) {
+ $("#entropybar-msg").html(msg);
+ if (msg.length === 0) {
+ return $("#entropybar-msg").addClass("entropyHidden");
+ } else {
+ return $("#entropybar-msg").removeClass("entropyHidden");
+ }
+ };
+
+ setResult = function(field, result) {
+ var ref, ref1;
+ ppolicyResults[field] = result;
+ $("#" + field).removeClass('fa-times fa-check fa-spinner fa-pulse fa-info-circle fa-question-circle text-danger text-success text-info text-secondary');
+ $("#" + field).attr('role', 'status');
+ switch (result) {
+ case "good":
+ $("#" + field).addClass('fa-check text-success');
+ break;
+ case "bad":
+ $("#" + field).addClass('fa-times text-danger');
+ $("#" + field).attr('role', 'alert');
+ break;
+ case "unknown":
+ $("#" + field).addClass('fa-question-circle text-secondary');
+ break;
+ case "waiting":
+ $("#" + field).addClass('fa-spinner fa-pulse text-secondary');
+ break;
+ case "info":
+ $("#" + field).addClass('fa-info-circle text-info');
+ }
+ if (Object.values(ppolicyResults).every((function(_this) {
+ return function(value) {
+ return value === "good" || value === "info";
+ };
+ })(this))) {
+ $('.ppolicy').removeClass('border-danger').addClass('border-success');
+ return (ref = $('#newpassword').get(0)) != null ? ref.setCustomValidity('') : void 0;
+ } else {
+ $('.ppolicy').removeClass('border-success').addClass('border-danger');
+ return (ref1 = $('#newpassword').get(0)) != null ? ref1.setCustomValidity("Insufficient quality") : void 0;
+ }
+ };
+
+ similar_text = function(first, second, percent) {
+ // discuss at: https://locutus.io/php/similar_text/
+ // original by: RafaĆ Kukawski (https://blog.kukawski.pl)
+ // bugfixed by: Chris McMacken
+ // bugfixed by: Jarkko Rantavuori original by findings in stackoverflow (https://stackoverflow.com/questions/14136349/how-does-similar-text-work)
+ // improved by: Markus Padourek (taken from https://www.kevinhq.com/2012/06/php-similartext-function-in-javascript_16.html)
+ // MIT licenses
+ // example 1: similar_text('Hello World!', 'Hello locutus!')
+ // returns 1: 8
+ // example 2: similar_text('Hello World!', null)
+ // returns 2: 0
+
+ if (first === null ||
+ second === null ||
+ typeof first === 'undefined' ||
+ typeof second === 'undefined') {
+ return 0
+ }
+
+ first += ''
+ second += ''
+
+ let pos1 = 0
+ let pos2 = 0
+ let max = 0
+ const firstLength = first.length
+ const secondLength = second.length
+ let p
+ let q
+ let l
+ let sum
+
+ for (p = 0; p < firstLength; p++) {
+ for (q = 0; q < secondLength; q++) {
+ for (l = 0; (p + l < firstLength) && (q + l < secondLength) && (first.charAt(p + l) === second.charAt(q + l)); l++) {
+ // @todo: ^-- break up this crazy for loop and put the logic in its body
+ }
+ if (l > max) {
+ max = l
+ pos1 = p
+ pos2 = q
+ }
+ }
+ }
+
+ sum = max
+
+ if (sum) {
+ if (pos1 && pos2) {
+ sum += similar_text(first.substr(0, pos1), second.substr(0, pos2))
+ }
+
+ if ((pos1 + max < firstLength) && (pos2 + max < secondLength)) {
+ sum += similar_text(
+ first.substr(pos1 + max, firstLength - pos1 - max),
+ second.substr(pos2 + max,
+ secondLength - pos2 - max))
+ }
+ }
+
+ if (!percent) {
+ return sum
+ }
+
+ return (sum * 200) / (firstLength + secondLength)
+ }
+
+
+ // Generic feature for checkpassword action
+ // check all local policy criteria one by one and display an appropriate button for each
+ $(document).on('checkpassword', function(event, context) {
+ var digit, evType, hasforbidden, i, len, lower, numspechar, password, report, setResult, upper;
+ password = context.password;
+ evType = context.evType;
+ setResult = context.setResult;
+ report = function(result, id) {
+ if (result) {
+ return setResult(id, "good");
+ } else {
+ return setResult(id, "bad");
+ }
+ };
+
+ removePPolicyCriteria = function(criteria, feedback) {
+ // first consider the criteria as fullfilled
+ report( true , feedback);
+ // remove criteria from the list of ppolicy checks
+ delete local_policy[criteria];
+ // remove the tag parent to given feedback
+ $( "#" + feedback ).parent().remove();
+ };
+
+
+ // Criteria checks
+ if (local_policy.pwd_min_length > 0) {
+ report(password.length >= local_policy.pwd_min_length, 'ppolicy-pwd_min_length-feedback');
+ }
+
+ if (local_policy.pwd_max_length > 0) {
+ report(password.length <= local_policy.pwd_max_length, 'ppolicy-pwd_max_length-feedback');
+ }
+
+ if (local_policy.pwd_min_upper > 0) {
+ upper = password.match(/[A-Z]/g);
+ report(upper && upper.length >= local_policy.pwd_min_upper, 'ppolicy-pwd_min_upper-feedback');
+ }
+
+ if (local_policy.pwd_min_lower > 0) {
+ lower = password.match(/[a-z]/g);
+ report(lower && lower.length >= local_policy.pwd_min_lower, 'ppolicy-pwd_min_lower-feedback');
+ }
+
+ if (local_policy.pwd_min_digit > 0) {
+ digit = password.match(/[0-9]/g);
+ report(digit && digit.length >= local_policy.pwd_min_digit, 'ppolicy-pwd_min_digit-feedback');
+ }
+
+ if (local_policy.pwd_no_reuse && local_policy.pwd_no_reuse == true) {
+ if( $( "#oldpassword" ).length )
+ {
+ oldpassword = $( "#oldpassword" ).val();
+ report( password != oldpassword , 'ppolicy-pwd_no_reuse-feedback');
+ }
+ else
+ {
+ removePPolicyCriteria("pwd_no_reuse", 'ppolicy-pwd_no_reuse-feedback');
+ }
+ }
+
+ if (local_policy.pwd_diff_login && local_policy.pwd_diff_login == true) {
+ loginElement = $( loginField.get(ltbComponent) );
+ if(loginElement.is("input"))
+ {
+ login = loginElement.val().trim();
+ }
+ else
+ {
+ login = loginElement.text().trim();
+ }
+ if( login.length )
+ {
+ report( password != login, 'ppolicy-pwd_diff_login-feedback');
+ }
+ else
+ {
+ report( true , 'ppolicy-pwd_diff_login-feedback');
+ }
+ }
+
+ if (local_policy.pwd_diff_last_min_chars > 0) {
+ if( $( "#oldpassword" ).length )
+ {
+ minDiffChars = local_policy.pwd_diff_last_min_chars;
+ oldpassword = $( "#oldpassword" ).val();
+
+ similarities = similar_text(oldpassword, password);
+ check_len = oldpassword.length < password.length ? oldpassword.length : password.length;
+ new_chars = check_len - similarities;
+ report( new_chars > minDiffChars , 'ppolicy-pwd_diff_last_min_chars-feedback');
+ }
+ else
+ {
+ removePPolicyCriteria("pwd_diff_last_min_chars", 'ppolicy-pwd_diff_last_min_chars-feedback');
+ }
+ }
+
+ if (local_policy.pwd_forbidden_chars) {
+ forbiddenChars = local_policy.pwd_forbidden_chars;
+ forbidden = false;
+ i = 0;
+ while (i < password.length) {
+ if (forbiddenChars.indexOf(password.charAt(i)) != -1) {
+ forbidden = true;
+ }
+ i++;
+ }
+ report( !forbidden, 'ppolicy-pwd_forbidden_chars-feedback' );
+ }
+
+ if (local_policy.pwd_min_special > 0 && local_policy.pwd_special_chars) {
+ numspechar = 0;
+ var re = new RegExp("["+local_policy.pwd_special_chars+"]","");
+ i = 0;
+ while (i < password.length) {
+ if (password.charAt(i).match(re)) {
+ numspechar++;
+ }
+ i++;
+ }
+ report(numspechar >= local_policy.pwd_min_special, 'ppolicy-pwd_min_special-feedback');
+ }
+
+ if ( local_policy.pwd_no_special_at_ends &&
+ local_policy.pwd_no_special_at_ends == true &&
+ local_policy.pwd_special_chars ) {
+ var re_start = new RegExp("^["+local_policy.pwd_special_chars+"]","");
+ var re_end = new RegExp("["+local_policy.pwd_special_chars+"]$","");
+ report( ( !password.match(re_start) && !password.match(re_end) ) , 'ppolicy-pwd_no_special_at_ends-feedback');
+ }
+
+ if ( local_policy.pwd_complexity) {
+ complexity = 0;
+ if (local_policy.pwd_special_chars) {
+ var re = new RegExp("["+local_policy.pwd_special_chars+"]","");
+ if( password.match(re) ){
+ complexity++;
+ }
+ }
+ if( password.match(/[A-Z]/g) ){
+ complexity++;
+ }
+ if( password.match(/[a-z]/g) ){
+ complexity++;
+ }
+ if( password.match(/[0-9]/g) ){
+ complexity++;
+ }
+ report( complexity >= local_policy.pwd_complexity, 'ppolicy-pwd_complexity-feedback');
+ }
+
+
+ if ( local_policy.use_pwnedpasswords) {
+ setResult('ppolicy-use_pwnedpasswords-feedback', "info");
+ }
+
+ });
+
+
+
+ // Specific feature for checkentropy action
+ $(document).on('checkpassword', function(event, context) {
+ var entropyrequired, entropyrequiredlevel, evType, newpasswordVal, password, setResult;
+ password = context.password;
+ evType = context.evType;
+ setResult = context.setResult;
+ if ($('#ppolicy-checkentropy-feedback').length > 0) {
+ newpasswordVal = $("#newpassword").val();
+ entropyrequired = $("span[trspan='checkentropyLabel']").attr("data-checkentropy_required");
+ entropyrequiredlevel = $("span[trspan='checkentropyLabel']").attr("data-checkentropy_required_level");
+ if (newpasswordVal.length === 0) {
+ displayEntropyBar("Err");
+ displayEntropyBarMsg("");
+ setResult('ppolicy-checkentropy-feedback', "unknown");
+ }
+ if (newpasswordVal.length > 0) {
+ return $.ajax({
+ dataType: "json",
+ url: location.pathname + entropyPath.get(ltbComponent),
+ method: "POST",
+ data: { "password": btoa(newpasswordVal) },
+ context: document.body,
+ success: function(data) {
+ var level, msg;
+ level = data.level;
+ msg = data.message;
+ if (level !== void 0) {
+ if (parseInt(level) >= 0 && parseInt(level) <= 4) {
+ displayEntropyBar(level);
+ displayEntropyBarMsg(msg);
+ if (entropyrequired === "1" && entropyrequiredlevel.length > 0) {
+ if (parseInt(level) >= parseInt(entropyrequiredlevel)) {
+ setResult('ppolicy-checkentropy-feedback', "good");
+ } else {
+ setResult('ppolicy-checkentropy-feedback', "bad");
+ }
+ }
+ if (entropyrequired !== "1") {
+ return setResult('ppolicy-checkentropy-feedback', "good");
+ }
+ } else if (parseInt(level) === -1) {
+ displayEntropyBar(level);
+ displayEntropyBarMsg(msg);
+ return setResult('ppolicy-checkentropy-feedback', "bad");
+ } else {
+ displayEntropyBar(level);
+ displayEntropyBarMsg(msg);
+ return setResult('ppolicy-checkentropy-feedback', "unknown");
+ }
+ }
+ },
+ error: function(j, status, err) {
+ var res;
+ if (err) {
+ console.log('checkentropy: frontend error: ', err);
+ }
+ if (j) {
+ res = JSON.parse(j.responseText);
+ }
+ if (res && res.error) {
+ return console.log('checkentropy: returned error: ', res);
+ }
+ }
+ });
+ }
+ }
+ });
+
+
+
+ checkpassword = function(password, evType) {
+ var e, info;
+ e = jQuery.Event("checkpassword");
+ info = {
+ password: password,
+ evType: evType,
+ setResult: setResult
+ };
+ return $(document).trigger(e, info);
+ };
+ if ( (local_policy != null) && $('#newpassword').length) {
+ checkpassword('');
+ $('#newpassword').keyup(function(e) {
+ checkpassword(e.target.value);
+ });
+ $('#newpassword').focusout(function(e) {
+ checkpassword(e.target.value, "focusout");
+ });
+ }
+
+}).call(this);
diff --git a/tests/Ltb/PpolicyTest.php b/tests/Ltb/PpolicyTest.php
new file mode 100644
index 0000000..09de6a1
--- /dev/null
+++ b/tests/Ltb/PpolicyTest.php
@@ -0,0 +1,432 @@
+ true,
+ "pwd_min_length" => 6,
+ "pwd_max_length" => 12,
+ "pwd_min_lower" => 1,
+ "pwd_min_upper" => 1,
+ "pwd_min_digit" => 1,
+ "pwd_min_special" => 1,
+ "pwd_special_chars" => "^a-zA-Z0-9",
+ "pwd_forbidden_chars" => "@",
+ "pwd_no_reuse" => true,
+ "pwd_diff_last_min_chars" => 0,
+ "pwd_diff_login" => true,
+ "pwd_complexity" => 0,
+ "use_pwnedpasswords" => false,
+ "pwd_no_special_at_ends" => false,
+ "pwd_forbidden_words" => array(),
+ "pwd_forbidden_ldap_fields"=> array(),
+ );
+
+ $login = "coudot";
+ $oldpassword = "secret";
+ $entry_array = array('cn' => array('common name'), 'sn' => array('surname'), 'customPasswordField' => array("{SSHA}7JWaNGUygodHyWt+DwPpOuYMDdKYJQQX"));
+ $change_custompwdfield = array(
+ array(
+ 'pwd_policy_config' => array(
+ 'pwd_no_reuse' => true,
+ 'pwd_unique_across_custom_password_fields' => true
+ ),
+ 'attribute' => 'customPasswordField',
+ 'hash' => "auto"
+ )
+ );
+ $change_custompwdfield2 = array(
+ array(
+ 'pwd_policy_config' => array(
+ 'pwd_no_reuse' => true,
+ 'pwd_unique_across_custom_password_fields' => true
+ ),
+ 'attribute' => 'customPasswordField',
+ 'hash' => "SSHA"
+ )
+ );
+
+ $this->assertEquals("sameaslogin", \Ltb\Ppolicy::check_password_strength( "coudot", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("sameasold", \Ltb\Ppolicy::check_password_strength( "secret", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("forbiddenchars", \Ltb\Ppolicy::check_password_strength( "p@ssword", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("minspecial", \Ltb\Ppolicy::check_password_strength( "password", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("mindigit", \Ltb\Ppolicy::check_password_strength( "!password", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("minupper", \Ltb\Ppolicy::check_password_strength( "!1password", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("minlower", \Ltb\Ppolicy::check_password_strength( "!1PASSWORD", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("toobig", \Ltb\Ppolicy::check_password_strength( "!1verylongPassword", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("tooshort", \Ltb\Ppolicy::check_password_strength( "!1Pa", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("sameascustompwd", \Ltb\Ppolicy::check_password_strength( "!TestMe123!", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("sameascustompwd", \Ltb\Ppolicy::check_password_strength( "!TestMe123!", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield2 ) );
+
+
+ $pwd_policy_config = array(
+ "pwd_show_policy" => true,
+ "pwd_min_length" => 6,
+ "pwd_max_length" => 12,
+ "pwd_min_lower" => 0,
+ "pwd_min_upper" => 0,
+ "pwd_min_digit" => 0,
+ "pwd_min_special" => 0,
+ "pwd_special_chars" => "^a-zA-Z0-9",
+ "pwd_forbidden_chars" => "@",
+ "pwd_no_reuse" => true,
+ "pwd_diff_last_min_chars" => 3,
+ "pwd_diff_login" => true,
+ "pwd_complexity" => 3,
+ "use_pwnedpasswords" => false,
+ "pwd_no_special_at_ends" => true,
+ "pwd_forbidden_words" => array('companyname', 'trademark'),
+ "pwd_forbidden_ldap_fields"=> array('cn', 'sn'),
+ );
+
+ $this->assertEquals("notcomplex", \Ltb\Ppolicy::check_password_strength( "simple", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("specialatends", \Ltb\Ppolicy::check_password_strength( "!simple", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("specialatends", \Ltb\Ppolicy::check_password_strength( "simple?", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("forbiddenwords", \Ltb\Ppolicy::check_password_strength( "companyname", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("forbiddenwords", \Ltb\Ppolicy::check_password_strength( "trademark", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("forbiddenwords", \Ltb\Ppolicy::check_password_strength( "working at companyname is fun", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("forbiddenldapfields", \Ltb\Ppolicy::check_password_strength( "common name", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("forbiddenldapfields", \Ltb\Ppolicy::check_password_strength( "my surname", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("diffminchars", \Ltb\Ppolicy::check_password_strength( "C0mplex", "C0mplexC0mplex", $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("", \Ltb\Ppolicy::check_password_strength( "C0mplex", "", $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("", \Ltb\Ppolicy::check_password_strength( "C0mplex", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("", \Ltb\Ppolicy::check_password_strength( "C0!mplex", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ $this->assertEquals("", \Ltb\Ppolicy::check_password_strength( "%C0!mplex", $oldpassword, $pwd_policy_config, $login, $entry_array, $change_custompwdfield ) );
+ }
+
+ /**
+ * Test check_password_strength function with pwned passwords
+ */
+ public function testCheckPasswordStrengthPwnedPasswords()
+ {
+
+ $login = "coudot";
+ $oldpassword = "secret";
+
+ if ( version_compare(PHP_VERSION, '7.2.5') >= 0 ) {
+ $pwd_policy_config = array(
+ "pwd_show_policy" => true,
+ "pwd_min_length" => 6,
+ "pwd_max_length" => 12,
+ "pwd_min_lower" => 1,
+ "pwd_min_upper" => 1,
+ "pwd_min_digit" => 1,
+ "pwd_min_special" => 1,
+ "pwd_special_chars" => "^a-zA-Z0-9",
+ "pwd_forbidden_chars" => "@",
+ "pwd_no_reuse" => true,
+ "pwd_diff_last_min_chars" => 0,
+ "pwd_diff_login" => true,
+ "pwd_complexity" => 0,
+ "use_pwnedpasswords" => true,
+ "pwd_no_special_at_ends" => false,
+ "pwd_forbidden_words" => array(),
+ "pwd_forbidden_ldap_fields"=> array(),
+ );
+
+ $this->assertEquals("pwned", \Ltb\Ppolicy::check_password_strength( "!1Password", $oldpassword, $pwd_policy_config, $login, array(), array() ) );
+ }
+
+ }
+
+ /**
+ * Test check_password_strength function with weak entropy password
+ */
+ public function testCheckPasswordStrengthWeakEntropy()
+ {
+
+ $login = "johnsmith";
+ $oldpassword = "secret";
+
+ if ( version_compare(PHP_VERSION, '7.2.5') >= 0 ) {
+ $pwd_policy_config = array(
+ "pwd_show_policy" => true,
+ "pwd_min_length" => 6,
+ "pwd_max_length" => 0,
+ "pwd_min_lower" => 0,
+ "pwd_min_upper" => 0,
+ "pwd_min_digit" => 0,
+ "pwd_min_special" => 0,
+ "pwd_special_chars" => "^a-zA-Z0-9",
+ "pwd_forbidden_chars" => "",
+ "pwd_no_reuse" => false,
+ "pwd_diff_last_min_chars" => 0,
+ "pwd_diff_login" => false,
+ "pwd_complexity" => 0,
+ "use_pwnedpasswords" => false,
+ "pwd_no_special_at_ends" => false,
+ "pwd_forbidden_words" => array(),
+ "pwd_forbidden_ldap_fields"=> array(),
+ "pwd_display_entropy" => true,
+ "pwd_check_entropy" => true,
+ "pwd_min_entropy" => 3
+ );
+
+ $this->assertEquals("insufficiententropy", \Ltb\Ppolicy::check_password_strength( "secret", $oldpassword, $pwd_policy_config, $login, array(), array() ) );
+ }
+
+ }
+
+ /**
+ * Test check_password_strength function with strong entropy password
+ */
+ public function testCheckPasswordStrengthStrongEntropy()
+ {
+
+ $login = "johnsmith";
+ $oldpassword = "secret";
+
+ if ( version_compare(PHP_VERSION, '7.2.5') >= 0 ) {
+ $pwd_policy_config = array(
+ "pwd_show_policy" => true,
+ "pwd_min_length" => 6,
+ "pwd_max_length" => 0,
+ "pwd_min_lower" => 0,
+ "pwd_min_upper" => 0,
+ "pwd_min_digit" => 0,
+ "pwd_min_special" => 0,
+ "pwd_special_chars" => "^a-zA-Z0-9",
+ "pwd_forbidden_chars" => "",
+ "pwd_no_reuse" => false,
+ "pwd_diff_last_min_chars" => 0,
+ "pwd_diff_login" => false,
+ "pwd_complexity" => 0,
+ "use_pwnedpasswords" => false,
+ "pwd_no_special_at_ends" => false,
+ "pwd_forbidden_words" => array(),
+ "pwd_forbidden_ldap_fields"=> array(),
+ "pwd_display_entropy" => true,
+ "pwd_check_entropy" => true,
+ "pwd_min_entropy" => 3
+ );
+
+ $this->assertEquals("", \Ltb\Ppolicy::check_password_strength( "Th!Sis@Str0ngP@ss0rd", $oldpassword, $pwd_policy_config, $login, array(), array() ) );
+ }
+
+ }
+
+ /**
+ * Test checkEntropyJSON function
+ */
+ public function testCheckEntropyJSON()
+ {
+
+ $password_weak = "secret";
+ $password_weak_base64 = base64_encode($password_weak);
+
+ $password_strong = "jtK8hEhNgT3wwGiDY_z7XmI92fUbnemQ";
+ $password_strong_base64 = base64_encode($password_strong);
+
+ $result_error = json_encode(
+ array(
+ "level" => "-1",
+ "message" => "missing parameter password"
+ )
+ );
+
+ $result_weak = json_encode(
+ array(
+ "level" => "0",
+ "message" => "This is a top-100 common password"
+ )
+ );
+
+ $result_strong = json_encode(
+ array(
+ "level" => "4",
+ "message" => ""
+ )
+ );
+
+ $this->assertEquals($result_error, \Ltb\Ppolicy::checkEntropyJSON( "" ) );
+ $this->assertEquals($result_weak, \Ltb\Ppolicy::checkEntropyJSON( $password_weak_base64 ) );
+ $this->assertEquals($result_strong, \Ltb\Ppolicy::checkEntropyJSON( $password_strong_base64 ) );
+
+ }
+
+ public function test_smarty_assign_variable()
+ {
+
+ # Password policy array
+ $pwd_policy_config = array(
+ "pwd_show_policy" => "always",
+ "pwd_min_length" => 12,
+ "pwd_max_length" => 32,
+ "pwd_min_lower" => 1,
+ "pwd_min_upper" => 1,
+ "pwd_min_digit" => 0,
+ "pwd_min_special" => 0,
+ "pwd_special_chars" => "^a-zA-Z0-9",
+ "pwd_forbidden_chars" => null,
+ "pwd_no_reuse" => true,
+ "pwd_diff_last_min_chars" => 0,
+ "pwd_diff_login" => true,
+ "pwd_complexity" => 3,
+ "use_pwnedpasswords" => true,
+ "pwd_no_special_at_ends" => false,
+ "pwd_forbidden_words" => array("secret","password"),
+ "pwd_forbidden_ldap_fields" => array(),
+ "pwd_display_entropy" => true,
+ "pwd_check_entropy" => true,
+ "pwd_min_entropy" => 3
+ );
+
+ $smarty = Mockery::mock('Smarty');
+
+ 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->shouldreceive('assign')
+ ->once()
+ ->with($param, $value);
+ }
+ }
+ }
+
+ \Ltb\Ppolicy::smarty_assign_variable($smarty, $pwd_policy_config);
+
+ $this->assertNotNull($smarty, "smarty variable is null while testing smarty_assign_variable" );
+
+ }
+
+ public function test_smarty_assign_ppolicy()
+ {
+
+ # Password policy array
+ $pwd_policy_config = array(
+ "pwd_show_policy" => "always",
+ "pwd_min_length" => 12,
+ "pwd_max_length" => 32,
+ "pwd_min_lower" => 1,
+ "pwd_min_upper" => 1,
+ "pwd_min_digit" => 0,
+ "pwd_min_special" => 0,
+ "pwd_special_chars" => "^a-zA-Z0-9",
+ "pwd_forbidden_chars" => null,
+ "pwd_no_reuse" => true,
+ "pwd_diff_last_min_chars" => 0,
+ "pwd_diff_login" => true,
+ "pwd_complexity" => 3,
+ "use_pwnedpasswords" => true,
+ "pwd_no_special_at_ends" => false,
+ "pwd_forbidden_words" => array("secret","password"),
+ "pwd_forbidden_ldap_fields" => array(),
+ "pwd_display_entropy" => true,
+ "pwd_check_entropy" => true,
+ "pwd_min_entropy" => 3
+ );
+
+ $smarty = Mockery::mock('Smarty');
+ $pwd_show_policy_pos = "above";
+ $pwd_show_policy = "always";
+ $result = "";
+
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("pwd_show_policy_pos", $pwd_show_policy_pos );
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("pwd_show_policy", $pwd_show_policy );
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("pwd_show_policy_onerror", true );
+
+ 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->shouldreceive('assign')
+ ->once()
+ ->with($param, $value);
+ }
+ }
+ }
+
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("json_policy", base64_encode(json_encode( $pwd_policy_config )) );
+
+ \Ltb\Ppolicy::smarty_assign_ppolicy($smarty, $pwd_show_policy_pos, $pwd_show_policy, $result, $pwd_policy_config);
+
+ $this->assertNotNull($smarty, "smarty variable is null while testing smarty_assign_ppolicy" );
+
+ }
+
+ public function test_smarty_assign_ppolicy_show_policy_onerror()
+ {
+
+ $pwd_policy_config = array();
+
+ $smarty = Mockery::mock('Smarty');
+ $pwd_show_policy_pos = "above";
+ $pwd_show_policy = "onerror";
+ $result = "forbiddenchars";
+
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("pwd_show_policy_pos", $pwd_show_policy_pos );
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("pwd_show_policy", $pwd_show_policy );
+ $smarty->shouldreceive('assign')
+ ->twice()
+ ->with("pwd_show_policy_onerror", true );
+
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("json_policy", base64_encode(json_encode( $pwd_policy_config )) );
+
+ \Ltb\Ppolicy::smarty_assign_ppolicy($smarty, $pwd_show_policy_pos, $pwd_show_policy, $result, $pwd_policy_config);
+
+ $this->assertNotNull($smarty, "smarty variable is null while testing smarty_assign_ppolicy" );
+
+ }
+
+ public function test_smarty_assign_ppolicy_show_policy_onerror_dummy()
+ {
+
+ $pwd_policy_config = array();
+
+ $smarty = Mockery::mock('Smarty');
+ $pwd_show_policy_pos = "above";
+ $pwd_show_policy = "onerror";
+ $result = "dummy";
+
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("pwd_show_policy_pos", $pwd_show_policy_pos );
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("pwd_show_policy", $pwd_show_policy );
+ $smarty->shouldreceive('assign')
+ ->with("pwd_show_policy_onerror", true );
+ $smarty->shouldreceive('assign')
+ ->with("pwd_show_policy_onerror", false );
+
+ $smarty->shouldreceive('assign')
+ ->once()
+ ->with("json_policy", base64_encode(json_encode( $pwd_policy_config )) );
+
+ \Ltb\Ppolicy::smarty_assign_ppolicy($smarty, $pwd_show_policy_pos, $pwd_show_policy, $result, $pwd_policy_config);
+
+ $this->assertNotNull($smarty, "smarty variable is null while testing smarty_assign_ppolicy" );
+
+ }
+
+}