diff options
-rw-r--r-- | CHANGES | 3 | ||||
-rw-r--r-- | docs/lib/passlib.hash.ldap_std.rst | 15 | ||||
-rw-r--r-- | passlib/handlers/ldap_digests.py | 48 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 33 |
4 files changed, 84 insertions, 15 deletions
@@ -50,6 +50,9 @@ Release History that sometimes occurred on platforms with a deviant implementation of :func:`!os_crypt`. + * The :doc:`ldap salted digests </lib/passlib.hash.ldap_std>` + now support salts from 4-16 bytes [issue 30]. + * All hashes will now throw :exc:`~passlib.exc.PasswordSizeError` if the provided password is larger than 4096 characters. diff --git a/docs/lib/passlib.hash.ldap_std.rst b/docs/lib/passlib.hash.ldap_std.rst index 3ae8c33..9418335 100644 --- a/docs/lib/passlib.hash.ldap_std.rst +++ b/docs/lib/passlib.hash.ldap_std.rst @@ -80,7 +80,7 @@ These hashes have the format :samp:`{prefix}{data}`. * :samp:`{prefix}` is `{SMD5}` for ldap_salted_md5, and `{SSHA}` for ldap_salted_sha1. * :samp:`{data}` is the base64 encoding of :samp:`{checksum}{salt}`; - and in turn :samp:`{salt}` is a 4 byte binary salt, + and in turn :samp:`{salt}` is a multi-byte binary salt, and :samp:`{checksum}` is the raw digest of the the string :samp:`{password}{salt}`, using the appropriate digest algorithm. @@ -113,13 +113,22 @@ Plaintext This handler does not hash passwords at all, rather it encoded them into UTF-8. -The only difference between this class and :class:`passlib.hash.plaintext` -is that this class will NOT recognize any strings using +The only difference between this class and :class:`~passlib.hash.plaintext` +is that this class will NOT recognize any strings that use the ``{SCHEME}HASH`` format. +Deviations +========== + +* The salt size for the salted digests appears to vary between applications. + While OpenLDAP is fixed at 4 bytes, some systems appear to use 8 or more. + Passlib can accept and generate strings with salts between 4-16 bytes, + though various servers may differ in what they can handle. .. rubric:: Footnotes .. [#pwd] The manpage for :command:`slappasswd` - `<http://gd.tuwien.ac.at/linuxcommand.org/man_pages/slappasswd8.html>`_. .. [#rfc] The basic format for these hashes is laid out in RFC 2307 - `<http://www.ietf.org/rfc/rfc2307.txt>`_ + +.. [#] OpenLDAP hash documentation - `<http://www.openldap.org/doc/admin24/security.html>`_ diff --git a/passlib/handlers/ldap_digests.py b/passlib/handlers/ldap_digests.py index ce251eb..5c8d9cf 100644 --- a/passlib/handlers/ldap_digests.py +++ b/passlib/handlers/ldap_digests.py @@ -38,8 +38,6 @@ __all__ = [ #========================================================= #ldap helpers #========================================================= -#reference - http://www.openldap.org/doc/admin24/security.html - class _Base64DigestHelper(uh.StaticHandler): "helper for ldap_md5 / ldap_sha1" #XXX: could combine this with hex digests in digests.py @@ -61,15 +59,23 @@ class _Base64DigestHelper(uh.StaticHandler): class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): "helper for ldap_salted_md5 / ldap_salted_sha1" - setting_kwds = ("salt",) + setting_kwds = ("salt", "salt_size") checksum_chars = uh.PADDED_BASE64_CHARS ident = None #required - prefix identifier + checksum_size = None #required _hash_func = None #required - hash function _hash_regex = None #required - regexp to recognize hash _stub_checksum = None #required - default checksum to plug in 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): if not hash: @@ -79,9 +85,13 @@ class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHand m = cls._hash_regex.match(hash) if not m: raise ValueError("not a %s hash" % (cls.name,)) - data = b64decode(m.group("tmp").encode("ascii")) - chk, salt = data[:-4], data[-4:] - return cls(checksum=chk, salt=salt) + try: + data = b64decode(m.group("tmp").encode("ascii")) + except TypeError: + raise ValueError("malformed %s hash" % (cls.name,)) + cs = cls.checksum_size + assert cs + return cls(checksum=data[:cs], salt=data[cs:]) def to_string(self): data = (self.checksum or self._stub_checksum) + self.salt @@ -125,37 +135,51 @@ class ldap_sha1(_Base64DigestHelper): 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-byte salt. + It supports a 4-16 byte salt. The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keyword: :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). - If specified, it must be a 4 byte string; each byte may have any value from 0x00 .. 0xff. + If specified, it may be any 4-16 byte string. + + :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. """ name = "ldap_salted_md5" ident = u("{SMD5}") + checksum_size = 16 _hash_func = md5 - _hash_regex = re.compile(u(r"^\{SMD5\}(?P<tmp>[+/a-zA-Z0-9]{27}=)$")) + _hash_regex = re.compile(u(r"^\{SMD5\}(?P<tmp>[+/a-zA-Z0-9]{27,}={0,2})$")) _stub_checksum = b('\x00') * 16 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-byte salt. + It supports a 4-16 byte salt. The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keyword: :param salt: Optional salt string. If not specified, one will be autogenerated (this is recommended). - If specified, it must be a 4 byte string; each byte may have any value from 0x00 .. 0xff. + If specified, it may be any 4-16 byte string. + + :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. """ name = "ldap_salted_sha1" ident = u("{SSHA}") + checksum_size = 20 _hash_func = sha1 - _hash_regex = re.compile(u(r"^\{SSHA\}(?P<tmp>[+/a-zA-Z0-9]{32})$")) + _hash_regex = re.compile(u(r"^\{SSHA\}(?P<tmp>[+/a-zA-Z0-9]{32,}={0,2})$")) _stub_checksum = b('\x00') * 20 class ldap_plaintext(plaintext): diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 5b32a60..6584f9e 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -840,6 +840,23 @@ class ldap_salted_md5_test(HandlerCase): known_correct_hashes = [ ("testing1234", '{SMD5}UjFY34os/pnZQ3oQOzjqGu4yeXE='), (UPASS_TABLE, '{SMD5}Z0ioJ58LlzUeRxm3K6JPGAvBGIM='), + + # alternate salt sizes (8, 15, 16) + ('test', '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw'), + ('test', '{SMD5}XRlncfRzvGi0FDzgR98tUgBg7B3jXOs9p9S615qTkg=='), + ('test', '{SMD5}FbAkzOMOxRbMp6Nn4hnZuel9j9Gas7a2lvI+x5hT6j0='), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SMD5}IGVhwK+anvspmfDt2t0vgGjt/Q==', + + # incorrect base64 encoding + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4c', + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw' + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P4cw=', + '{SMD5}LnuZPJhiaY95/4lmV=pg548xBsD4P4cw', + '{SMD5}LnuZPJhiaY95/4lmVFpg548xBsD4P===', ] class ldap_salted_sha1_test(HandlerCase): @@ -848,6 +865,22 @@ class ldap_salted_sha1_test(HandlerCase): ("testing123", '{SSHA}0c0blFTXXNuAMHECS4uxrj3ZieMoWImr'), ("secret", "{SSHA}0H+zTv8o4MR4H43n03eCsvw1luG8LdB7"), (UPASS_TABLE, '{SSHA}3yCSD1nLZXznra4N8XzZgAL+s1sQYsx5'), + + # alternate salt sizes (8, 15, 16) + ('test', '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOckw=='), + ('test', '{SSHA}/ZMF5KymNM+uEOjW+9STKlfCFj51bg3BmBNCiPHeW2ttbU0='), + ('test', '{SSHA}Pfx6Vf48AT9x3FVv8znbo8WQkEVSipHSWovxXmvNWUvp/d/7'), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SSHA}ZQK3Yvtvl6wtIRoISgMGPkcWU7Nfq5U=', + + # incorrect base64 encoding + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOck', + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOckw=', + '{SSHA}P90+qijSp8MJ1tN25j5o1Pf=UvlqjXHOGeOckw==', + '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOck===', ] class ldap_plaintext_test(HandlerCase): |