]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Adding optional rounds and salt to bin/set-ldap-password
authorFrank Brehm <frank@brehm-online.com>
Fri, 11 Nov 2022 12:45:54 +0000 (13:45 +0100)
committerFrank Brehm <frank@brehm-online.com>
Fri, 11 Nov 2022 12:45:54 +0000 (13:45 +0100)
lib/pp_admintools/app/set_ldap_password.py
lib/pp_admintools/handler/ldap_password.py

index f59a0386a939c04e091b189b72c8176674609d58..14fc4392dba41255205b132eea390fbb31d5117e 100644 (file)
@@ -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)
 
index aab4912ba9d228e17cb736585b830a0e9abab210..c15b7c519a0dbfd2ce742ef87777f597034af5f7 100644 (file)
@@ -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
 
     # -------------------------------------------------------------------------