From a5358cdf9477df724f0abb7bd6c8b65c869fb50c Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Fri, 23 Sep 2022 16:15:28 +0200 Subject: [PATCH] Cleaning up password hashing methods, starting underlaying password change --- lib/pp_admintools/app/set_ldap_password.py | 187 +++++++++++++++++++-- 1 file changed, 172 insertions(+), 15 deletions(-) diff --git a/lib/pp_admintools/app/set_ldap_password.py b/lib/pp_admintools/app/set_ldap_password.py index 7320f00..4dea773 100644 --- a/lib/pp_admintools/app/set_ldap_password.py +++ b/lib/pp_admintools/app/set_ldap_password.py @@ -20,7 +20,7 @@ import passlib.apps # Own modules # from fb_tools.common import to_bool, is_sequence, pp -from fb_tools.common import is_sequence +from fb_tools.common import is_sequence, pp from ..xlate import XLATOR @@ -28,7 +28,7 @@ from .ldap import LdapAppError from .ldap import BaseLdapApplication from .ldap import PasswordFileOptionAction -__version__ = '0.4.1' +__version__ = '0.5.1' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -55,17 +55,14 @@ class SetLdapPasswordApplication(BaseLdapApplication): possible_schemes = ( 'ldap_des_crypt', - 'ldap_bcrypt', 'ldap_md5', 'ldap_md5_crypt', 'ldap_salted_md5', 'ldap_sha1', 'ldap_sha1_crypt', 'ldap_salted_sha1', - 'ldap_pbkdf2_sha1', 'ldap_sha256_crypt', 'ldap_salted_sha256', - 'ldap_pbkdf2_sha256', 'ldap_sha512_crypt', 'ldap_salted_sha512', 'ldap_pbkdf2_sha512', @@ -76,24 +73,40 @@ class SetLdapPasswordApplication(BaseLdapApplication): schema_ids = { 'ldap_des_crypt': 'CRYPT', - 'ldap_bcrypt': 'BCRYPT', 'ldap_md5': 'MD5', - 'ldap_md5_crypt': 'MD5-CRYPT', + 'ldap_md5_crypt': 'CRYPT-MD5', 'ldap_salted_md5': 'SMD5', 'ldap_sha1': 'SHA', 'ldap_sha1_crypt': 'SHA-CRYPT', 'ldap_salted_sha1': 'SSHA', - 'ldap_pbkdf2_sha1': 'PBKDF2-SHA', - 'ldap_sha256_crypt': 'SHA256-CRYPT', + 'ldap_sha256_crypt': 'CRYPT-SHA256', 'ldap_salted_sha256': 'SSHA256', - 'ldap_pbkdf2_sha256': 'PBKDF2-SHA256', - 'ldap_sha512_crypt': 'SHA512-CRYPT', + 'ldap_sha512_crypt': 'CRYPT-SHA512', 'ldap_salted_sha512': 'SSHA512', - 'ldap_pbkdf2_sha512': 'PBKDF2-SHA512', + 'ldap_pbkdf2_sha512': 'PBKDF2_SHA512', + } + + schema_description = { + 'ldap_des_crypt': _('The ancient and notorious 3 DES crypt method.'), + 'ldap_md5': _('Pure {} hashing method.').format('MD5'), + 'ldap_md5_crypt': _("A {} based hashing algorithm.").format('MD5'), + 'ldap_salted_md5': _("Salted {} hashing method.").format('MD5'), + 'ldap_sha1': _('Pure {} hashing method.').format('SHA-1'), + 'ldap_sha1_crypt': _("A {} based hashing algorithm.").format('SHA-1'), + 'ldap_salted_sha1': _("Salted {} hashing method.").format('SHA-1'), + 'ldap_sha256_crypt': _("A {} based hashing algorithm.").format('SHA-256'), + 'ldap_salted_sha256': _("Salted {} hashing method.").format('SHA-256'), + 'ldap_sha512_crypt': _("A {} based hashing algorithm.").format('SHA-512'), + 'ldap_salted_sha512': _("Salted {} hashing method.").format('SHA-512'), + 'ldap_pbkdf2_sha512': _( + "A hashing method derived from {} with additional computing rounds.").format( + 'SHA-512'), } passlib_context = None default_schema = 'ldap_salted_sha256' + default_schema_id = 'SSHA256' + default_pbkdf2_rounds = 30000 # ------------------------------------------------------------------------- @classmethod @@ -106,7 +119,8 @@ class SetLdapPasswordApplication(BaseLdapApplication): if schema in all_handlers: cls.available_schemes.append(schema) - cls.passlib_context = passlib.context.CryptContext(schemes=cls.available_schemes) + cls.passlib_context = passlib.context.CryptContext( + schemes=cls.available_schemes, ldap_pbkdf2_sha512__rounds=cls.default_pbkdf2_rounds) cls.passlib_context.update(default=cls.default_schema) # ------------------------------------------------------------------------- @@ -120,12 +134,14 @@ class SetLdapPasswordApplication(BaseLdapApplication): self.current_password = None self.need_current_password = False + self.current_password_hash = None self.do_user_bind = False self.ask_for_password = False self.new_password = None self.user_uid = None self.user_dn = None - self.schema = None + self.schema = self.default_schema + self.schema_id = self.default_schema_id my_appname = self.get_generic_appname(appname) @@ -196,6 +212,24 @@ class SetLdapPasswordApplication(BaseLdapApplication): "asked for it.").format(_("PASSWORD")), ) + schema_list = [] + def_schema = '' + for method in self.available_schemes: + schema_id = self.schema_ids[method] + schema_list.append(schema_id) + if method == self.default_schema: + def_schema = schema_id + schema_list.append('list') + + app_group.add_argument( + '-S', '--schema', metavar=_("SCHEMA"), dest="schema", choices=schema_list, + help=_( + "The schema (hashing method) to use to hash the new password. " + "It is possible to give here the value {val_list!r}, then all possible schemes " + "are shown and exit. Default: {default!r}.").format( + val_list='list', default=def_schema) + ) + user_help = _( "The user, which password in the given LDAP instance should be changed. " "It may be given by its Uid (the alphanumeric POSIX name), its mail address " @@ -225,6 +259,25 @@ class SetLdapPasswordApplication(BaseLdapApplication): super(SetLdapPasswordApplication, self).post_init() + if self.verbose > 5: + msg = "Given args:\n" + pp(self.args.__dict__) + LOG.debug(msg) + + given_schema = getattr(self.args, 'schema', None) + if given_schema: + if given_schema == 'list': + self._show_hashing_schemes() + self.exit(0) + return + for method in self.available_schemes: + schema_id = self.schema_ids[method] + LOG.debug("Testing for {m!r} ({s}) ...".format(m=method, s=schema_id)) + if schema_id == given_schema: + self.passlib_context.update(default=method) + self.schema = method + self.schema_id = schema_id + break + given_user = getattr(self.args, 'user', None) if given_user: self.user_uid = given_user @@ -260,6 +313,36 @@ class SetLdapPasswordApplication(BaseLdapApplication): if not ldap.is_admin or ldap.readonly: self.do_user_bind = True + # ------------------------------------------------------------------------- + def _show_hashing_schemes(self): + + max_len_schema = 1 + for method in self.available_schemes: + schema_id = self.schema_ids[method] + if len(schema_id) > max_len_schema: + max_len_schema = len(schema_id) + + title = _("Usable Hashing schemes:") + print(title) + print('-' * len(title)) + print() + + for method in self.available_schemes: + schema_id = self.schema_ids[method] + desc = self.schema_description[method] + if 'pbkdf2' in method: + desc += ' ' + _( + "This schema cannot be used for authentication on a " + "current freeradius server.") + if method == self.schema: + desc += ' ' + _("This is the default schema.") + + line = ' * {id:<{max_len}} - '.format(id=schema_id, max_len=max_len_schema) + line += desc + print(line) + + print() + # ------------------------------------------------------------------------- def pre_run(self): @@ -274,7 +357,7 @@ class SetLdapPasswordApplication(BaseLdapApplication): msg = _("Using LDAP instance {inst!r} - {url}.").format(inst=inst, url=connect_info.url) LOG.info(msg) - self.user_dn = self.search_user_dn() + self.search_user_dn() if self.do_user_bind and not self.current_password: first_prompt = _("Current password of user {!r}:").format(self.user_uid) + ' ' @@ -291,6 +374,9 @@ class SetLdapPasswordApplication(BaseLdapApplication): self.new_password = self.get_password( first_prompt, second_prompt, may_empty=False, repeat=True) + self.get_current_password_hash() + self.do_set_password() + # ------------------------------------------------------------------------- def test_user_bind(self): @@ -332,6 +418,35 @@ class SetLdapPasswordApplication(BaseLdapApplication): connect_info.url)) del ldap_server + # ------------------------------------------------------------------------- + def get_current_password_hash(self): + + inst = self.ldap_instances[0] + connect_info = self.cfg.ldap_connection[inst] + + if self.verbose > 1: + msg = _( + "Trying to get current password hash of user {!r} ...").format(self.user_dn) + LOG.debug(msg) + + attributes = ['carLicense', 'ppFirstPassword', 'userPassword'] + + entry = self.get_entry(self.user_dn, inst, attributes=attributes) + + if not entry: + msg = _("User with DN {dn!r} not found in {uri}.").format( + dn=self.user_dn, uri=connect_info.url) + LOG.error(msg) + self.exit(6) + return None + + cur_pwd_hash = None + attribs = self.normalized_attributes(entry) + if attribs['userPassword']: + cur_pwd_hash = attribs['userPassword'][0] + + self.current_password_hash = cur_pwd_hash + # ------------------------------------------------------------------------- def search_user_dn(self): """Searching the LDAP DN of the user, whos password should be changed.""" @@ -369,6 +484,48 @@ class SetLdapPasswordApplication(BaseLdapApplication): LOG.info(_("Changing the password of user {dn!r} in LDAP instance {inst}.").format( dn=self.user_dn, inst=connect_info.url)) + # ------------------------------------------------------------------------- + def do_set_password(self): + + print() + msg = _("Setting password of {dn!r} with hashing schema {schema!r}.").format( + dn=self.user_dn, schema=self.schema_id) + msg = _("Setting password of '{dn}' with hashing schema '{schema}' ...").format( + dn=self.colored(self.user_dn, 'CYAN'), schema=self.colored(self.schema_id, 'CYAN')) + print(msg) + + if self.current_password_hash: + msg = _("Current password hash: '{}'.").format( + self.colored(self.current_password_hash, 'CYAN')) + else: + msg = _("The user '{}' has currently no password.").format( + self.colored(self.user_dn, 'CYAN')) + print(msg) + + LOG.debug(_("Used schema: {!r}.").format(self.schema)) + hashed_passwd = self.passlib_context.hash(self.new_password, self.schema) + msg = _("New password hash: '{}'.").format(self.colored(hashed_passwd, 'CYAN')) + print(msg) + + print() + msg = _("Apply new password? [{yes}/{no}]?").format( + yes=self.colored(_('yes'), 'RED'), no=self.colored(_('No'), 'GREEN')) + ' ' + do_set_passwd = False + if self.yes: + do_set_passwd = True + else: + do_set_passwd = self.ask_for_yes_or_no(msg, default_on_empty=False) + print() + + if not do_set_passwd: + msg = _("Do not setting password for {!r}.").format(self.user_dn) + LOG.info(msg) + self.exit(0) + return + + msg = _("Setting password ...") + LOG.info(msg) + # ============================================================================= if __name__ == "__main__": -- 2.39.5