import logging
# Third party modules
+import passlib.apps
+
+HAS_CRACKLIB = False
+try:
+ import cracklib
+ HAS_CRACKLIB = True
+except ImportError:
+ pass
+
from fb_tools.handling_obj import HandlingObject
from fb_tools.errors import FbHandlerError
_ = XLATOR.gettext
ngettext = XLATOR.ngettext
-__version__ = '0.1.0'
+__version__ = '0.2.0'
# =============================================================================
pass
+# =============================================================================
+class WrongPwdSchemaError(FbHandlerError):
+ """Exception class for given wrong schema on update."""
+
+ # -------------------------------------------------------------------------
+ def __init__(self, schema):
+
+ self.schema = schema
+
+ # -------------------------------------------------------------------------
+ def __str__(self):
+ """Typecast into a string."""
+ return _("Encryption schema {!r} inot found.").format(self.schema)
+
+
# =============================================================================
class LdapPasswordHandler(HandlingObject):
"""Handler class for handling LDAP passwords."""
+ possible_schemes = (
+ 'ldap_des_crypt',
+ 'ldap_md5',
+ 'ldap_md5_crypt',
+ 'ldap_salted_md5',
+ 'ldap_sha1',
+ 'ldap_sha1_crypt',
+ 'ldap_salted_sha1',
+ 'ldap_sha256_crypt',
+ 'ldap_salted_sha256',
+ 'ldap_sha512_crypt',
+ 'ldap_salted_sha512',
+ 'ldap_pbkdf2_sha512',
+ )
+
+ available_schemes = []
+
+ schema_ids = {
+ 'ldap_des_crypt': 'CRYPT',
+ 'ldap_md5': 'MD5',
+ 'ldap_md5_crypt': 'CRYPT-MD5',
+ 'ldap_salted_md5': 'SMD5',
+ 'ldap_sha1': 'SHA',
+ 'ldap_sha1_crypt': 'SHA-CRYPT',
+ 'ldap_salted_sha1': 'SSHA',
+ 'ldap_sha256_crypt': 'CRYPT-SHA256',
+ 'ldap_salted_sha256': 'SSHA256',
+ 'ldap_sha512_crypt': 'CRYPT-SHA512',
+ 'ldap_salted_sha512': 'SSHA512',
+ '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'),
+ }
+
+ 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'
+
+ # -------------------------------------------------------------------------
+ @classmethod
+ def init_pass_schemes(cls):
+
+ cls.available_schemes = []
+ all_handlers = passlib.registry.list_crypt_handlers()
+
+ for schema in cls.possible_schemes:
+ if schema in all_handlers:
+ cls.available_schemes.append(schema)
+
+ context_opts = {
+ 'schemes': cls.available_schemes,
+ 'default': cls.default_schema,
+ }
+
+ for schema in cls.default_rounds:
+ key = schema + '__rounds'
+ context_opts[key] = cls.default_rounds[key]
+
+ cls.passlib_context = passlib.context.CryptContext(**context_opts)
+
# -------------------------------------------------------------------------
def __init__(
self, appname=None, verbose=0, version=__version__, base_dir=None,
terminal_has_colors=terminal_has_colors, initialized=False,
)
+ # -------------------------------------------------------------------------
+ def as_dict(self, short=True):
+ """
+ Transforms the elements of the object into a dict
+
+ @param short: don't include local properties in resulting dict.
+ @type short: bool
+
+ @return: structure as dict
+ @rtype: dict
+ """
+
+ res = super(LdapPasswordHandler, self).as_dict(short=short)
+
+ res['available_schemes'] = self.available_schemes
+ res['default_schema'] = self.passlib_context.default_scheme()
+ res['schema_ids'] = self.schema_ids
+
+ return res
+
+ # -------------------------------------------------------------------------
+ @classmethod
+ def update_schema(
+ cls, schema, rounds=None, default_rounds=None, vary_rounds=None,
+ min_rounds=None, max_rounds=None):
+
+ if schema not in cls.available_schemes:
+ msg = _("Invalid schema {!r} given for update.").format(schema)
+ raise LdapPwdHandlerError(msg)
+
+ update_args = {}
+
+ if rounds:
+ key = "{}__rounds".format(schema)
+ update_args[key] = rounds
+
+ if default_rounds:
+ key = "{}__default_rounds".format(schema)
+ update_args[key] = default_rounds
+
+ if vary_rounds:
+ key = "{}__vary_rounds".format(schema)
+ update_args[key] = vary_rounds
+
+ if min_rounds:
+ key = "{}__min_rounds".format(schema)
+ update_args[key] = min_rounds
+
+ if max_rounds:
+ key = "{}__max_rounds".format(schema)
+ update_args[key] = max_rounds
+
+ if update_args:
+ cls.passlib_context.update(**update_args)
+
+ # -------------------------------------------------------------------------
+ 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 set_schema(self, schema):
+
+ if schema not in self.available_schemes:
+ raise WrongPwdSchemaError(schema)
+
+ self.schema_id = self.schema_ids[schema]
+ self.schema = schema
+ self.passlib_context.update(default=schema)
+
+ # -------------------------------------------------------------------------
+ def set_schema_by_id(self, given_schema_id):
+
+ found = False
+
+ for schema in self.available_schemes:
+ schema_id = self.schema_ids[schema]
+ LOG.debug("Testing for {m!r} ({s}) ...".format(m=schema, s=schema_id))
+ if schema_id == given_schema_id:
+ self.passlib_context.update(default=schema)
+ self.schema = schema
+ self.schema_id = schema_id
+ found = True
+ break
+
+ if not found:
+ raise WrongPwdSchemaError(given_schema_id)
+
+ # -------------------------------------------------------------------------
+ def get_hash(self, password, schema=None):
+
+ hashed_passwd = self.passlib_context.hash(password, self.schema)
+ return hashed_passwd
+
+ # -------------------------------------------------------------------------
+ def check_password_quality(self, password):
+
+ if not HAS_CRACKLIB:
+ msg = _(
+ "Cannot testing the quality of the new password, because the "
+ "Python module {!r} is not installed.").format('cracklib')
+ LOG.warn(msg)
+ return True
+
+ LOG.info(_("Testing quality of new password ..."))
+ try:
+ cracklib.VeryFascistCheck(password)
+ except ValueError as e:
+ msg = _("Quality of the new password is not sufficient:") + ' ' + str(e)
+ LOG.error(msg)
+ return False
+
+ LOG.debug("The quality of the new password seems to be sufficient.")
+ return True
+
# vim: ts=4 et list