"""passlib.handlers.digests - plain hash digests """ #============================================================================= # imports #============================================================================= # core from base64 import b64encode, b64decode from hashlib import md5, sha1, sha256, sha512 import logging; log = logging.getLogger(__name__) import re # site # pkg from passlib.handlers.misc import plaintext from passlib.utils import unix_crypt_schemes, to_unicode from passlib.utils.decor import classproperty import passlib.utils.handlers as uh # local __all__ = [ "ldap_plaintext", "ldap_md5", "ldap_sha1", "ldap_salted_md5", "ldap_salted_sha1", "ldap_salted_sha256", "ldap_salted_sha512", ##"get_active_ldap_crypt_schemes", "ldap_des_crypt", "ldap_bsdi_crypt", "ldap_md5_crypt", "ldap_sha1_crypt", "ldap_bcrypt", "ldap_sha256_crypt", "ldap_sha512_crypt", ] #============================================================================= # ldap helpers #============================================================================= class _Base64DigestHelper(uh.StaticHandler): """helper for ldap_md5 / ldap_sha1""" # XXX: could combine this with hex digests in digests.py ident = None # required - prefix identifier _hash_func = None # required - hash function _hash_regex = None # required - regexp to recognize hash checksum_chars = uh.PADDED_BASE64_CHARS @classproperty def _hash_prefix(cls): """tell StaticHandler to strip ident from checksum""" return cls.ident def _calc_checksum(self, secret): if isinstance(secret, str): secret = secret.encode("utf-8") chk = self._hash_func(secret).digest() return b64encode(chk).decode("ascii") class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): """helper for ldap_salted_md5 / ldap_salted_sha1""" setting_kwds = ("salt", "salt_size") checksum_chars = uh.PADDED_BASE64_CHARS ident = None # required - prefix identifier _hash_func = None # required - hash function _hash_regex = None # required - regexp to recognize hash min_salt_size = max_salt_size = 4 # NOTE: openldap implementation uses 4 byte salt, # but it's been reported (issue 30) that some servers use larger salts. # the semi-related rfc3112 recommends support for up to 16 byte salts. min_salt_size = 4 default_salt_size = 4 max_salt_size = 16 @classmethod def from_string(cls, hash): hash = to_unicode(hash, "ascii", "hash") m = cls._hash_regex.match(hash) if not m: raise uh.exc.InvalidHashError(cls) try: data = b64decode(m.group("tmp").encode("ascii")) except TypeError: raise uh.exc.MalformedHashError(cls) cs = cls.checksum_size assert cs return cls(checksum=data[:cs], salt=data[cs:]) def to_string(self): data = self.checksum + self.salt hash = self.ident + b64encode(data).decode("ascii") return hash def _calc_checksum(self, secret): if isinstance(secret, str): secret = secret.encode("utf-8") return self._hash_func(secret + self.salt).digest() #============================================================================= # implementations #============================================================================= class ldap_md5(_Base64DigestHelper): """This class stores passwords using LDAP's plain MD5 format, and follows the :ref:`password-hash-api`. The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. """ name = "ldap_md5" ident = u"{MD5}" _hash_func = md5 _hash_regex = re.compile(r"^\{MD5\}(?P[+/a-zA-Z0-9]{22}==)$") class ldap_sha1(_Base64DigestHelper): """This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`. The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods have no optional keywords. """ name = "ldap_sha1" ident = u"{SHA}" _hash_func = sha1 _hash_regex = re.compile(r"^\{SHA\}(?P[+/a-zA-Z0-9]{27}=)$") class ldap_salted_md5(_SaltedBase64DigestHelper): """This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`. It supports a 4-16 byte salt. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: bytes :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it may be any 4-16 byte string. :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 4 bytes for compatibility with the LDAP spec, but some systems use larger salts, and Passlib supports any value between 4-16. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.6 .. versionchanged:: 1.6 This format now supports variable length salts, instead of a fix 4 bytes. """ name = "ldap_salted_md5" ident = u"{SMD5}" checksum_size = 16 _hash_func = md5 _hash_regex = re.compile(r"^\{SMD5\}(?P[+/a-zA-Z0-9]{27,}={0,2})$") class ldap_salted_sha1(_SaltedBase64DigestHelper): """ This class stores passwords using LDAP's "Salted SHA1" format, and follows the :ref:`password-hash-api`. It supports a 4-16 byte salt. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: bytes :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it may be any 4-16 byte string. :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 4 bytes for compatibility with the LDAP spec, but some systems use larger salts, and Passlib supports any value between 4-16. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.6 .. versionchanged:: 1.6 This format now supports variable length salts, instead of a fix 4 bytes. """ name = "ldap_salted_sha1" ident = u"{SSHA}" checksum_size = 20 _hash_func = sha1 # NOTE: 32 = ceil((20 + 4) * 4/3) _hash_regex = re.compile(r"^\{SSHA\}(?P[+/a-zA-Z0-9]{32,}={0,2})$") class ldap_salted_sha256(_SaltedBase64DigestHelper): """ This class stores passwords using LDAP's "Salted SHA2-256" format, and follows the :ref:`password-hash-api`. It supports a 4-16 byte salt. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: bytes :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it may be any 4-16 byte string. :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 8 bytes for compatibility with the LDAP spec, but Passlib supports any value between 4-16. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.7.3 """ name = "ldap_salted_sha256" ident = u"{SSHA256}" checksum_size = 32 default_salt_size = 8 _hash_func = sha256 # NOTE: 48 = ceil((32 + 4) * 4/3) _hash_regex = re.compile(r"^\{SSHA256\}(?P[+/a-zA-Z0-9]{48,}={0,2})$") class ldap_salted_sha512(_SaltedBase64DigestHelper): """ This class stores passwords using LDAP's "Salted SHA2-512" format, and follows the :ref:`password-hash-api`. It supports a 4-16 byte salt. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords: :type salt: bytes :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). If specified, it may be any 4-16 byte string. :type salt_size: int :param salt_size: Optional number of bytes to use when autogenerating new salts. Defaults to 8 bytes for compatibility with the LDAP spec, but Passlib supports any value between 4-16. :type relaxed: bool :param relaxed: By default, providing an invalid value for one of the other keywords will result in a :exc:`ValueError`. If ``relaxed=True``, and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning` will be issued instead. Correctable errors include ``salt`` strings that are too long. .. versionadded:: 1.7.3 """ name = "ldap_salted_sha512" ident = u"{SSHA512}" checksum_size = 64 default_salt_size = 8 _hash_func = sha512 # NOTE: 91 = ceil((64 + 4) * 4/3) _hash_regex = re.compile(r"^\{SSHA512\}(?P[+/a-zA-Z0-9]{91,}={0,2})$") class ldap_plaintext(plaintext): """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. This class acts much like the generic :class:`!passlib.hash.plaintext` handler, except that it will identify a hash only if it does NOT begin with the ``{XXX}`` identifier prefix used by RFC2307 passwords. The :meth:`~passlib.ifc.PasswordHash.hash`, :meth:`~passlib.ifc.PasswordHash.genhash`, and :meth:`~passlib.ifc.PasswordHash.verify` methods all require the following additional contextual keyword: :type encoding: str :param encoding: This controls the character encoding to use (defaults to ``utf-8``). This encoding will be used to encode :class:`!str` passwords under Python 2, and decode :class:`!bytes` hashes under Python 3. .. versionchanged:: 1.6 The ``encoding`` keyword was added. """ # NOTE: this subclasses plaintext, since all it does differently # is override identify() name = "ldap_plaintext" _2307_pat = re.compile(r"^\{\w+\}.*$") @uh.deprecated_method(deprecated="1.7", removed="2.0") @classmethod def genconfig(cls): # Overridding plaintext.genconfig() since it returns "", # but have to return non-empty value due to identify() below return "!" @classmethod def identify(cls, hash): # NOTE: identifies all strings EXCEPT those with {XXX} prefix hash = uh.to_unicode_for_identify(hash) return bool(hash) and cls._2307_pat.match(hash) is None #============================================================================= # {CRYPT} wrappers # the following are wrappers around the base crypt algorithms, # which add the ldap required {CRYPT} prefix #============================================================================= ldap_crypt_schemes = [ 'ldap_' + name for name in unix_crypt_schemes ] def _init_ldap_crypt_handlers(): # NOTE: I don't like to implicitly modify globals() like this, # but don't want to write out all these handlers out either :) g = globals() for wname in unix_crypt_schemes: name = 'ldap_' + wname g[name] = uh.PrefixWrapper(name, wname, prefix=u"{CRYPT}", lazy=True) del g _init_ldap_crypt_handlers() ##_lcn_host = None ##def get_host_ldap_crypt_schemes(): ## global _lcn_host ## if _lcn_host is None: ## from passlib.hosts import host_context ## schemes = host_context.schemes() ## _lcn_host = [ ## "ldap_" + name ## for name in unix_crypt_names ## if name in schemes ## ] ## return _lcn_host #============================================================================= # eof #=============================================================================