From f979f92a1c5635aa6261e5a93932abfea69c804c Mon Sep 17 00:00:00 2001 From: Markus Beckschulte Date: Fri, 1 Mar 2024 17:38:30 +0100 Subject: [PATCH] bring password functions from ssp (#11) --- src/Ltb/Ldap.php | 132 +++++++++++++++ src/Ltb/Password.php | 317 +++++++++++++++++++++++++++++++++++++ src/Ltb/PhpLDAP.php | 27 +++- tests/Ltb/PasswordTest.php | 164 +++++++++++++++++++ 4 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 src/Ltb/Password.php create mode 100644 tests/Ltb/PasswordTest.php diff --git a/src/Ltb/Ldap.php b/src/Ltb/Ldap.php index e212d7e..9d367d2 100644 --- a/src/Ltb/Ldap.php +++ b/src/Ltb/Ldap.php @@ -136,7 +136,139 @@ static function sorted_search($ldap, $ldap_base, $ldap_filter, $attributes, $sor return array($ldap_result,$errno,$entries); } + + /** + * Gets the value of the password attribute + * @param \LDAP\Connection|array $ldap An LDAP\Connection instance, returned by ldap_connect() + * @param string $dn the dn of the user + * @param type $pwdattribute the Attribute that contains the password + * @return string the value of $pwdattribute + */ + static function get_password_value($ldap, $dn, $pwdattribute): string { + $search_userpassword = \Ltb\PhpLDAP::ldap_read($ldap, $dn, "(objectClass=*)", array($pwdattribute)); + if ($search_userpassword) { + return \Ltb\PhpLDAP::ldap_get_values($ldap, ldap_first_entry($ldap, $search_userpassword), $pwdattribute); + } + } + + /** + * Changes the password of an user while binded as the user in an Active Directory + * @param \LDAP\Connection|array $ldap An LDAP\Connection instance, returned by ldap_connect() + * @param string $dn the dn of the user + * @param string $oldpassword the old password + * @param string $password the new password + * @return array [$error_code, $error_msg] + */ + static function change_ad_password_as_user($ldap, $dn, $oldpassword, $password): array { + # The AD password change procedure is modifying the attribute unicodePwd by + # first deleting unicodePwd with the old password and them adding it with the + # the new password + $oldpassword_hashed = make_ad_password($oldpassword); + + $modifications = array( + array( + "attrib" => "unicodePwd", + "modtype" => LDAP_MODIFY_BATCH_REMOVE, + "values" => array($oldpassword_hashed), + ), + array( + "attrib" => "unicodePwd", + "modtype" => LDAP_MODIFY_BATCH_ADD, + "values" => array($password), + ), + ); + + \Ltb\PhpLDAP::ldap_modify_batch($ldap, $dn, $modifications); + $error_code = ldap_errno($ldap); + $error_msg = ldap_error($ldap); + return array($error_code, $error_msg); + } + + static protected function get_ppolicy_error_code($ctrls) { + if (isset($ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE])) { + $value = $ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']; + if (isset($value['error'])) { + $ppolicy_error_code = $value['error']; + error_log("LDAP - Ppolicy error code: $ppolicy_error_code"); + return $ppolicy_error_code; + } + } + return false; + } + + /** + * Changes the Password using extended password modification + * @param \LDAP\Connection|array $ldap An LDAP\Connection instance, returned by ldap_connect() + * @param string $dn the dn of the user + * @param string $oldpassword the old password + * @param string $password the new password + * @param array $userdata + * @param bool $use_ppolicy_control + * @return array 0: error_code, 1: error_msg, 2: ppolicy_error_code + */ + static function change_password_with_exop($ldap, $dn, $oldpassword, $password, $use_ppolicy_control): array { + $ppolicy_error_code = false; + $exop_passwd = FALSE; + if ( $use_ppolicy_control ) { + $ctrls = array(); + $exop_passwd = \Ltb\PhpLDAP::ldap_exop_passwd($ldap, $dn, $oldpassword, $password, $ctrls); + $error_code = \Ltb\PhpLDAP::ldap_errno($ldap); + $error_msg = \Ltb\PhpLDAP::ldap_error($ldap); + if (!$exop_passwd) { + $ppolicy_error_code = self::get_ppolicy_error_code($ctrls); + } + } else { + $exop_passwd = \Ltb\PhpLDAP::ldap_exop_passwd($ldap, $dn, $oldpassword, $password); + $error_code = \Ltb\PhpLDAP::ldap_errno($ldap); + $error_msg = \Ltb\PhpLDAP::ldap_error($ldap); + } + return array($error_code, $error_msg, $ppolicy_error_code); + } + + /** + * Changes attributes (and password) using Password Policy Control + * @param \LDAP\Connection|array $ldap An LDAP\Connection instance, returned by ldap_connect() + * @param string $dn the dn of the user + * @param array $userdata the array, containing the new (hashed) password + * @return array 0: error_code, 1: error_msg, 2: ppolicy_error_code + */ + static function modify_attributes_using_ppolicy($ldap, $dn, $userdata): array { + $error_code = ""; + $error_msg = ""; + $ctrls = array(); + $ppolicy_error_code = false; + $ppolicy_replace = \Ltb\PhpLDAP::ldap_mod_replace_ext($ldap, $dn, $userdata, [['oid' => LDAP_CONTROL_PASSWORDPOLICYREQUEST]]); + if (\Ltb\PhpLDAP::ldap_parse_result($ldap, $ppolicy_replace, $error_code, $matcheddn, $error_msg, $referrals, $ctrls)) { + $ppolicy_error_code = self::get_ppolicy_error_code($ctrls); + } + return array($error_code, $error_msg, $ppolicy_error_code); + } + + /** + * Changes attributes (and password) + * @param \LDAP\Connection|array $ldap An LDAP\Connection instance, returned by ldap_connect() + * @param string $dn the dn of the user + * @param array $userdata the array, containing the new (hashed) password + * @return array 0: error_code, 1: error_msg + */ + static function modify_attributes($ldap, $dn, $userdata): array { + \Ltb\PhpLDAP::ldap_mod_replace($ldap, $dn, $userdata); + $error_code = ldap_errno($ldap); + $error_msg = ldap_error($ldap); + return array($error_code, $error_msg); + } + const PPOLICY_ERROR_CODE_TO_RESULT_MAPPER = [ + 0 => "passwordExpired", + 1 => "accountLocked", + 2 => "changeAfterReset", + 3 => "passwordModNotAllowed", + 4 => "mustSupplyOldPassword", + 5 => "badquality", + 6 => "tooshort", + 7 => "tooyoung", + 8 => "inhistory" + ]; } ?> diff --git a/src/Ltb/Password.php b/src/Ltb/Password.php new file mode 100644 index 0000000..8cb519e --- /dev/null +++ b/src/Ltb/Password.php @@ -0,0 +1,317 @@ + 0 ) { + $userdata["sambaPwdCanChange"] = $time + ( $samba_options['min_age'] * 86400 ); + } + if ( isset($samba_options['max_age']) && $samba_options['max_age'] > 0 ) { + $userdata["sambaPwdMustChange"] = $time + ( $samba_options['max_age'] * 86400 ); + } + if ( isset($samba_options['expire_days']) && $samba_options['expire_days'] > 0 ) { + $userdata["sambaKickoffTime"] = $time + ( $samba_options['expire_days'] * 86400 ); + } + return $userdata; + } + + static function set_ad_data($userdata, $ad_options, $password): array { + $userdata["unicodePwd"] = $password; + if ( $ad_options['force_unlock'] ) { + $userdata["lockoutTime"] = 0; + } + if ( $ad_options['force_pwd_change'] ) { + $userdata["pwdLastSet"] = 0; + } + return $userdata; + } + + static function set_shadow_data($userdata, $shadow_options, $time): array { + if ( $shadow_options['update_shadowLastChange'] ) { + $userdata["shadowLastChange"] = floor($time / 86400); + } + + if ( $shadow_options['update_shadowExpire'] ) { + if ( $shadow_options['shadow_expire_days'] > 0) { + $userdata["shadowExpire"] = floor(($time / 86400) + $shadow_options['shadow_expire_days']); + } else { + $userdata["shadowExpire"] = $shadow_options['shadow_expire_days']; + } + } + return $userdata; + } + +} diff --git a/src/Ltb/PhpLDAP.php b/src/Ltb/PhpLDAP.php index 84a065c..57ea9f1 100644 --- a/src/Ltb/PhpLDAP.php +++ b/src/Ltb/PhpLDAP.php @@ -70,6 +70,31 @@ public static function ldap_count_entries($ldap, $result) { return ldap_count_entries($ldap, $result); } - + + public static function ldap_modify_batch($ldap, $dn, $modifications) + { + return ldap_modify_batch($ldap, $dn, $modifications); + } + + public static function ldap_exop_passwd($ldap, $dn, $oldpassword, $password, $ctrls) + { + return ldap_exop_passwd($ldap, $dn, $oldpassword, $password, $ctrls); + } + + public static function ldap_mod_replace_ext($ldap, $dn, $userdata, $ctrls) + { + return ldap_mod_replace_ext($ldap, $dn, $userdata, $ctrls); + } + + public static function ldap_parse_result($ldap, $ppolicy_replace, $error_code, $matcheddn, $error_msg, $referrals, $ctrls) + { + return ldap_parse_result($ldap, $ppolicy_replace, $error_code, $matcheddn, $error_msg, $referrals, $ctrls); + } + + public static function ldap_mod_replace($ldap, $dn, $userdata) + { + return ldap_mod_replace($ldap, $dn, $userdata); + } + } ?> diff --git a/tests/Ltb/PasswordTest.php b/tests/Ltb/PasswordTest.php new file mode 100644 index 0000000..f07cd9b --- /dev/null +++ b/tests/Ltb/PasswordTest.php @@ -0,0 +1,164 @@ + 10, 'crypt_salt_prefix' => "test"); + + foreach ($hash_algos as $algo) { + $hash = \Ltb\Password::make_password($originalPassword, $algo, $hash_options); + + $this->assertEquals(true, \Ltb\Password::check_password($originalPassword, $hash, $algo)); + $this->assertEquals(false, \Ltb\Password::check_password("notSamePassword", $hash, $algo)); + + $this->assertEquals(true, \Ltb\Password::check_password($originalPassword, $hash, "auto")); + $this->assertEquals(false, \Ltb\Password::check_password("notSamePassword", $hash, "auto")); + } + + //and NTLM, where "auto" does not work: + $hash = \Ltb\Password::make_md4_password($originalPassword); + $this->assertEquals(true, \Ltb\Password::check_password($originalPassword, $hash, "NTLM")); + $this->assertEquals(false, \Ltb\Password::check_password("notSamePassword", $hash, "NTLM")); + } + + function test_make_ad_password() { + $originalPassword = 'TestMe123!+*ä'; + $adpassword = \Ltb\Password::make_ad_password($originalPassword); + $this->assertEquals(true, mb_check_encoding($adpassword, "UTF-16LE")); + $this->assertEquals(pack("H*", '220054006500730074004d00650031003200330021002b002a00e4002200'), $adpassword); + } + + function test_set_samba_data() { + $password = 'TestMe123!+*ä'; + $time = 1711717971; + $userdata = ["userPassword" => 'TestMe123!+*ä']; + + $samba_options_min_age = ['min_age' => 10]; + $expected_userdata_samba_min_age = [ + "userPassword" => 'TestMe123!+*ä', + "sambaNTPassword" => \Ltb\Password::make_md4_password($password), + "sambaPwdLastSet" => $time, + "sambaPwdCanChange" => 1712581971 + ]; + $actual_userdata_samba_min_age = \Ltb\Password::set_samba_data($userdata, $samba_options_min_age, $password, $time); + $this->assertEquals($expected_userdata_samba_min_age, $actual_userdata_samba_min_age); + + $samba_options_max_age = ['max_age' => 10]; + $expected_userdata_samba_max_age = [ + "userPassword" => 'TestMe123!+*ä', + "sambaNTPassword" => \Ltb\Password::make_md4_password($password), + "sambaPwdLastSet" => $time, + "sambaPwdMustChange" => 1712581971 + ]; + $actual_userdata_samba_max_age = \Ltb\Password::set_samba_data($userdata, $samba_options_max_age, $password, $time); + $this->assertEquals($expected_userdata_samba_max_age, $actual_userdata_samba_max_age); + + $samba_options_expire_days = ['expire_days' => 10]; + $expected_userdata_samba_expire_days = [ + "userPassword" => 'TestMe123!+*ä', + "sambaNTPassword" => \Ltb\Password::make_md4_password($password), + "sambaPwdLastSet" => $time, + "sambaKickoffTime" => 1712581971 + ]; + $actual_userdata_samba_expire_days = \Ltb\Password::set_samba_data($userdata, $samba_options_expire_days, $password, $time); + $this->assertEquals($expected_userdata_samba_expire_days, $actual_userdata_samba_expire_days); + } + + function test_set_ad_data() { + $password = \Ltb\Password::make_ad_password('TestMe123!+*ä'); + $userdata = [ + "userPassword" => 'TestMe123!+*ä', + "sambaPwdCanChange" => 1712581971 + ]; + + $ad_options_force_unlock = [ + "force_unlock" => true, + "force_pwd_change" => false + ]; + $expected_userdata_force_unlock = [ + "userPassword" => 'TestMe123!+*ä', + "sambaPwdCanChange" => 1712581971, + "unicodePwd" => \Ltb\Password::make_ad_password('TestMe123!+*ä'), + "lockoutTime" => 0 + ]; + $actual_userdata_force_unlock = \Ltb\Password::set_ad_data($userdata, $ad_options_force_unlock, $password); + $this->assertEquals($expected_userdata_force_unlock, $actual_userdata_force_unlock); + + $ad_options_force_pwd_change = [ + "force_unlock" => false, + "force_pwd_change" => true + ]; + $expected_userdata_force_pwd_change = [ + "userPassword" => 'TestMe123!+*ä', + "sambaPwdCanChange" => 1712581971, + "unicodePwd" => \Ltb\Password::make_ad_password('TestMe123!+*ä'), + "pwdLastSet" => 0 + ]; + $actual_userdata_force_pwd_change = \Ltb\Password::set_ad_data($userdata, $ad_options_force_pwd_change, $password); + $this->assertEquals($expected_userdata_force_pwd_change, $actual_userdata_force_pwd_change); + } + + function test_set_shadow_data() { + $time = 1711717971; + $userdata = [ + "userPassword" => 'TestMe123!+*ä', + "sambaNTPassword" => \Ltb\Password::make_md4_password('TestMe123!+*ä'), + "sambaPwdLastSet" => $time, + "sambaPwdCanChange" => 1712581971 + ]; + + $shadow_options_update_shadowLastChange = [ + "update_shadowLastChange" => true, + "update_shadowExpire" => false + ]; + $expected_userdata_update_shadowLastChange = [ + "userPassword" => 'TestMe123!+*ä', + "sambaNTPassword" => \Ltb\Password::make_md4_password('TestMe123!+*ä'), + "sambaPwdLastSet" => $time, + "sambaPwdCanChange" => 1712581971, + "shadowLastChange" => 19811 + ]; + $actual_userdata_update_shadowLastChange = \Ltb\Password::set_shadow_data($userdata, $shadow_options_update_shadowLastChange, $time); + $this->assertEquals($expected_userdata_update_shadowLastChange, $actual_userdata_update_shadowLastChange); + + $shadow_options_update_shadowExpire = [ + "update_shadowLastChange" => false, + "update_shadowExpire" => true, + "shadow_expire_days" => 10 + ]; + $expected_userdata_update_shadowExpire = [ + "userPassword" => 'TestMe123!+*ä', + "sambaNTPassword" => \Ltb\Password::make_md4_password('TestMe123!+*ä'), + "sambaPwdLastSet" => $time, + "sambaPwdCanChange" => 1712581971, + "shadowExpire" => 19821 + ]; + $actual_userdata_update_shadowExpire = \Ltb\Password::set_shadow_data($userdata, $shadow_options_update_shadowExpire, $time); + $this->assertEquals($expected_userdata_update_shadowExpire, $actual_userdata_update_shadowExpire); + + $shadow_options_update_shadowExpire_negative = [ + "update_shadowLastChange" => false, + "update_shadowExpire" => true, + "shadow_expire_days" => -1 + ]; + $expected_userdata_update_shadowExpire_negative = [ + "userPassword" => 'TestMe123!+*ä', + "sambaNTPassword" => \Ltb\Password::make_md4_password('TestMe123!+*ä'), + "sambaPwdLastSet" => $time, + "sambaPwdCanChange" => 1712581971, + "shadowExpire" => -1 + ]; + $actual_userdata_update_shadowExpire_negative = \Ltb\Password::set_shadow_data($userdata, $shadow_options_update_shadowExpire_negative, $time); + $this->assertEquals($expected_userdata_update_shadowExpire_negative, $actual_userdata_update_shadowExpire_negative); + + + } +}