From 735302ac8731ee0bb0e035b1248c17057860fb99 Mon Sep 17 00:00:00 2001 From: David Coutadeur Date: Fri, 30 Aug 2024 15:13:06 +0200 Subject: [PATCH] add password policy functions to ltb-common (#36) * add functions check_password_strength and checkEntropyJSON from ssp * add html, css and js code * add functions smarty_assign_variable and smarty_assign_ppolicy --- composer.json | 4 +- src/Ltb/Ppolicy.php | 271 ++++++++++++++++++++++ src/ppolicy/css/ppolicy.css | 33 +++ src/ppolicy/html/policy.tpl | 130 +++++++++++ src/ppolicy/js/ppolicy.js | 388 ++++++++++++++++++++++++++++++++ tests/Ltb/PpolicyTest.php | 432 ++++++++++++++++++++++++++++++++++++ 6 files changed, 1257 insertions(+), 1 deletion(-) create mode 100644 src/Ltb/Ppolicy.php create mode 100644 src/ppolicy/css/ppolicy.css create mode 100644 src/ppolicy/html/policy.tpl create mode 100644 src/ppolicy/js/ppolicy.js create mode 100644 tests/Ltb/PpolicyTest.php 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} +
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" ); + + } + +}