summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES3
-rw-r--r--docs/lib/passlib.hash.ldap_std.rst15
-rw-r--r--passlib/handlers/ldap_digests.py48
-rw-r--r--passlib/tests/test_handlers.py33
4 files changed, 84 insertions, 15 deletions
diff --git a/CHANGES b/CHANGES
index 8fc1cc9..c329398 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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):