From: Frank Brehm Date: Fri, 11 Nov 2022 12:45:54 +0000 (+0100) Subject: Adding optional rounds and salt to bin/set-ldap-password X-Git-Tag: 0.7.2^2^2~11 X-Git-Url: https://git.uhu-banane.org/?a=commitdiff_plain;h=d6f93ce381fd6674717871515b9c5f615e530dfa;p=pixelpark%2Fpp-admin-tools.git Adding optional rounds and salt to bin/set-ldap-password --- diff --git a/lib/pp_admintools/app/set_ldap_password.py b/lib/pp_admintools/app/set_ldap_password.py index f59a038..14fc439 100644 --- a/lib/pp_admintools/app/set_ldap_password.py +++ b/lib/pp_admintools/app/set_ldap_password.py @@ -32,8 +32,9 @@ from .ldap import PasswordFileOptionAction from ..handler.ldap_password import WrongPwdSchemaError from ..handler.ldap_password import LdapPasswordHandler from ..handler.ldap_password import HAS_CRACKLIB +from ..handler.ldap_password import WrongSaltError, WrongRoundsError -__version__ = '0.7.2' +__version__ = '0.8.1' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -201,6 +202,20 @@ class SetLdapPasswordApplication(BaseLdapApplication): app_group.add_argument( 'user', metavar=_('USER'), help=user_help) + app_group.add_argument( + '--salt', metavar='SALT', dest="salt", + help=_( + "A possible salt to use on hashing the password. Caution: " + "not all hashing schemes are supporting a salt.") + ) + + app_group.add_argument( + '--rounds', metavar='ROUNDS', dest="rounds", type=int, + help=_( + "The number of calculation rounds to use on hashing the password. Caution: " + "not all hashing schemes are supporting calculation rounds.") + ) + super(SetLdapPasswordApplication, self).init_arg_parser() # ------------------------------------------------------------------------- @@ -460,8 +475,15 @@ class SetLdapPasswordApplication(BaseLdapApplication): self.colored(self.user_dn, 'CYAN')) print(msg) + salt = getattr(self.args, 'salt', None) + rounds = getattr(self.args, 'rounds', None) + LOG.debug(_("Used schema: {!r}.").format(self.pwd_handler.schema)) - hashed_passwd = self.pwd_handler.get_hash(self.new_password, self.pwd_handler.schema) + try: + hashed_passwd = self.pwd_handler.get_hash( + self.new_password, self.pwd_handler.schema, salt=salt, rounds=rounds) + except (WrongSaltError, WrongRoundsError) as e: + self.exit(1, str(e)) msg = _("New password hash: '{}'.").format(self.colored(hashed_passwd, 'CYAN')) print(msg) diff --git a/lib/pp_admintools/handler/ldap_password.py b/lib/pp_admintools/handler/ldap_password.py index aab4912..c15b7c5 100644 --- a/lib/pp_admintools/handler/ldap_password.py +++ b/lib/pp_admintools/handler/ldap_password.py @@ -20,6 +20,7 @@ try: except ImportError: pass +from fb_tools.common import to_str, to_bytes from fb_tools.handling_obj import HandlingObject from fb_tools.errors import FbHandlerError @@ -31,7 +32,7 @@ LOG = logging.getLogger(__name__) _ = XLATOR.gettext ngettext = XLATOR.ngettext -__version__ = '0.2.2' +__version__ = '0.3.1' # ============================================================================= @@ -55,6 +56,18 @@ class WrongPwdSchemaError(FbHandlerError): return _("Encryption schema {!r} inot found.").format(self.schema) +# ============================================================================= +class WrongSaltError(FbHandlerError): + """Exception class in case of a wrong salt.""" + pass + + +# ============================================================================= +class WrongRoundsError(FbHandlerError): + """Exception class in case of a wrong calculation rounds.""" + pass + + # ============================================================================= class LdapPasswordHandler(HandlingObject): """Handler class for handling LDAP passwords.""" @@ -108,12 +121,6 @@ class LdapPasswordHandler(HandlingObject): 'SHA-512'), } - default_rounds = { - 'ldap_sha256_crypt': 64000, - 'ldap_sha512_crypt': 64000, - 'ldap_pbkdf2_sha512': 30000, - } - passlib_context = None default_schema = 'ldap_sha512_crypt' default_schema_id = 'CRYPT-SHA512' @@ -134,10 +141,6 @@ class LdapPasswordHandler(HandlingObject): 'default': cls.default_schema, } - for schema in cls.default_rounds: - key = schema + '__rounds' - context_opts[key] = cls.default_rounds[schema] - cls.passlib_context = passlib.context.CryptContext(**context_opts) # ------------------------------------------------------------------------- @@ -160,19 +163,45 @@ class LdapPasswordHandler(HandlingObject): # ------------------------------------------------------------------------- @property - def salt_len(self): - """Gives the valid length of a salt string in dependency to the current schema.""" - if hasattr(self, 'schema') and self.schema == 'ldap_des_crypt': - return 2 - return 8 + def salt_info(self): + """Gives information about possible salt of the current schema.""" + if not hasattr(self.__class__, 'passlib_context'): + return None + + default_schema = self.passlib_context.default_scheme() + default_handler = self.passlib_context.handler(default_schema) + if 'salt' in default_handler.setting_kwds: + ret = { + 'min_len': default_handler.min_salt_size, + 'max_len': default_handler.max_salt_size, + 'default_size': default_handler.default_salt_size, + 'usable_chars': default_handler.salt_chars, + } + else: + ret = None + + return ret # ------------------------------------------------------------------------- @property - def salt(self): - """The salt of the current schema.""" + def rounds_info(self): + """Gives information about possible rounds parameter of the current schema.""" if not hasattr(self.__class__, 'passlib_context'): return None - return self.passlib_context.salt() + + default_schema = self.passlib_context.default_scheme() + default_handler = self.passlib_context.handler(default_schema) + if 'rounds' in default_handler.setting_kwds: + ret = { + 'min': default_handler.min_rounds, + 'max': default_handler.max_rounds, + 'default': default_handler.default_rounds, + 'costs': default_handler.rounds_cost, + } + else: + ret = None + + return ret # ------------------------------------------------------------------------- def as_dict(self, short=True): @@ -188,11 +217,15 @@ class LdapPasswordHandler(HandlingObject): res = super(LdapPasswordHandler, self).as_dict(short=short) + default_schema = self.passlib_context.default_scheme() + default_handler = self.passlib_context.handler(default_schema) + res['available_schemes'] = self.available_schemes res['passlib_context'] = self.passlib_context.to_dict(True) - res['default_schema'] = self.passlib_context.default_scheme() - # res['salt'] = self.salt - res['salt_len'] = self.salt_len + res['default_schema'] = default_schema + res['default_handler'] = default_handler + res['rounds_info'] = self.rounds_info + res['salt_info'] = self.salt_info res['schema_ids'] = self.schema_ids return res @@ -291,12 +324,86 @@ class LdapPasswordHandler(HandlingObject): raise WrongPwdSchemaError(given_schema_id) # ------------------------------------------------------------------------- - def get_hash(self, password, schema=None): + def verify_salt(self, salt, schema=None): if not schema: schema = self.schema - hashed_passwd = self.passlib_context.hash(password) + handler = self.passlib_context.handler(schema) + if 'salt' not in handler.setting_kwds: + msg = _("The password schema {!r} does not support a password salt.").format(schema) + raise WrongSaltError(msg) + + if len(salt) < handler.min_salt_size: + msg = _("The password salt must be at least by {} characters.").format( + handler.min_salt_size) + raise WrongSaltError(msg) + if len(salt) > handler.max_salt_size: + msg = _("The password salt may have a length of maximum {} characters.").format( + handler.max_salt_size) + raise WrongSaltError(msg) + + if self.verbose > 1: + LOG.debug("Usable characters: {!r}".format(handler.salt_chars)) + if isinstance(handler.salt_chars, (bytes, bytearray)): + salt = to_bytes(salt) + for character in salt: + if character not in handler.salt_chars: + msg = _("Found invalid character {!r} in password salt.").format(character) + raise WrongSaltError(msg) + + return salt + + # ------------------------------------------------------------------------ + def verify_rounds(self, rounds, schema=None): + + if not schema: + schema = self.schema + + handler = self.passlib_context.handler(schema) + if 'rounds' not in handler.setting_kwds: + msg = _("The password schema {!r} does not support calculation rounds.").format(schema) + raise WrongRoundsError(msg) + + try: + rounds = int(rounds) + except (TypeError, ValueError) as e: + msg = _("Wrong value {v!r} for calculation rounds: {e}").format(v=rounds, e=e) + raise WrongRoundsError(msg) + + if rounds < handler.min_rounds: + msg = _("The value for the calculation rounds has to be at least {}.").format( + handler.min_rounds) + raise WrongRoundsError(msg) + + if rounds > handler.max_rounds: + msg = _("The value for the calculation rounds has to at most {}.").format( + handler.max_rounds) + raise WrongRoundsError(msg) + + return rounds + + # ------------------------------------------------------------------------- + def get_hash(self, password, schema=None, salt=None, rounds=None): + + if not schema: + schema = self.schema + + add_opts = {} + + if salt: + if not isinstance(salt, str): + if isinstance(salt, (bytes, bytearray)): + salt = to_str(salt) + else: + salt = str(salt) + add_opts['salt'] = self.verify_salt(salt, schema) + + if rounds: + rounds = self.verify_rounds(rounds, schema) + add_opts['rounds'] = rounds + + hashed_passwd = self.passlib_context.hash(password, **add_opts) return hashed_passwd # -------------------------------------------------------------------------