diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2020-10-05 21:14:41 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2020-10-05 21:14:41 -0400 |
commit | 3f1ad211d9499f5c36e9cc90c888c6e0da50ab3c (patch) | |
tree | c01df999752b89a3c272c4c0e6acff52b2977ef6 | |
parent | a2809fd98ed58eb006d204b05cc1519bdf1b12de (diff) | |
download | passlib-3f1ad211d9499f5c36e9cc90c888c6e0da50ab3c.tar.gz |
passlib.hash: added ldap_salted_sha256 & ldap_salted_512 (issue 124)
-rw-r--r-- | docs/history/1.7.rst | 7 | ||||
-rw-r--r-- | docs/lib/passlib.hash.django_std.rst | 2 | ||||
-rw-r--r-- | docs/lib/passlib.hash.ldap_std.rst | 5 | ||||
-rw-r--r-- | docs/lib/passlib.hash.rst | 2 | ||||
-rw-r--r-- | passlib/apps.py | 14 | ||||
-rw-r--r-- | passlib/handlers/ldap_digests.py | 91 | ||||
-rw-r--r-- | passlib/hash.py | 2 | ||||
-rw-r--r-- | passlib/registry.py | 2 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 61 |
9 files changed, 178 insertions, 8 deletions
diff --git a/docs/history/1.7.rst b/docs/history/1.7.rst index c5b15d0..18bdae7 100644 --- a/docs/history/1.7.rst +++ b/docs/history/1.7.rst @@ -35,6 +35,13 @@ Administrative Changes The mailing list and documentation urls remain the same. +New Features +------------ + +* .. py:currentmodule:: passlib.hash + + :class:`ldap_salted_sha512`: LDAP "salted hash" support added for SHA-256 and SHA-512 (:issue:`124`). + Bugfixes -------- diff --git a/docs/lib/passlib.hash.django_std.rst b/docs/lib/passlib.hash.django_std.rst index 5135f4f..e733b56 100644 --- a/docs/lib/passlib.hash.django_std.rst +++ b/docs/lib/passlib.hash.django_std.rst @@ -128,8 +128,6 @@ Interface .. versionadded:: 1.6 -.. autoclass:: django_bcrypt_sha256() - Format ------ An example :class:`!django_pbkdf2_sha256` hash (of ``password``) is: diff --git a/docs/lib/passlib.hash.ldap_std.rst b/docs/lib/passlib.hash.ldap_std.rst index d3788c1..1245749 100644 --- a/docs/lib/passlib.hash.ldap_std.rst +++ b/docs/lib/passlib.hash.ldap_std.rst @@ -62,6 +62,8 @@ Salted Hashes ============= .. autoclass:: ldap_salted_md5() .. autoclass:: ldap_salted_sha1() +.. autoclass:: ldap_salted_sha256() +.. autoclass:: ldap_salted_sha512() These hashes have the format :samp:`{prefix}{data}`. @@ -95,6 +97,9 @@ The LDAP salted hashes should not be considered very secure. which is only borderline sufficient to defeat rainbow tables, and cannot (portably) be increased. +* The SHA2 salted hashes (SSHA256, SSHA512) are only marginally better. + they use the newer SHA2 hash; and 64 bits of entropy in their salt. + Plaintext ========= .. autoclass:: ldap_plaintext() diff --git a/docs/lib/passlib.hash.rst b/docs/lib/passlib.hash.rst index aaf7e60..0103a4f 100644 --- a/docs/lib/passlib.hash.rst +++ b/docs/lib/passlib.hash.rst @@ -168,6 +168,8 @@ and are supported by OpenLDAP. * :class:`passlib.hash.ldap_sha1` - SHA1 digest * :class:`passlib.hash.ldap_salted_md5` - salted MD5 digest * :class:`passlib.hash.ldap_salted_sha1` - salted SHA1 digest +* :class:`passlib.hash.ldap_salted_sha256` - salted SHA256 digest +* :class:`passlib.hash.ldap_salted_sha512` - salted SHA512 digest .. toctree:: :maxdepth: 1 diff --git a/passlib/apps.py b/passlib/apps.py index 7c4be06..30cd6af 100644 --- a/passlib/apps.py +++ b/passlib/apps.py @@ -124,9 +124,17 @@ django_context = django110_context #============================================================================= # ldap #============================================================================= -std_ldap_schemes = ["ldap_salted_sha1", "ldap_salted_md5", - "ldap_sha1", "ldap_md5", - "ldap_plaintext" ] + +#: standard ldap schemes +std_ldap_schemes = [ + "ldap_salted_sha512", + "ldap_salted_sha256", + "ldap_salted_sha1", + "ldap_salted_md5", + "ldap_sha1", + "ldap_md5", + "ldap_plaintext", +] # create context with all std ldap schemes EXCEPT crypt ldap_nocrypt_context = LazyCryptContext(std_ldap_schemes) diff --git a/passlib/handlers/ldap_digests.py b/passlib/handlers/ldap_digests.py index 17ab774..30254f0 100644 --- a/passlib/handlers/ldap_digests.py +++ b/passlib/handlers/ldap_digests.py @@ -5,7 +5,7 @@ #============================================================================= # core from base64 import b64encode, b64decode -from hashlib import md5, sha1 +from hashlib import md5, sha1, sha256, sha512 import logging; log = logging.getLogger(__name__) import re # site @@ -22,6 +22,8 @@ __all__ = [ "ldap_sha1", "ldap_salted_md5", "ldap_salted_sha1", + "ldap_salted_sha256", + "ldap_salted_sha512", ##"get_active_ldap_crypt_schemes", "ldap_des_crypt", @@ -160,7 +162,9 @@ class ldap_salted_md5(_SaltedBase64DigestHelper): _hash_regex = re.compile(u(r"^\{SMD5\}(?P<tmp>[+/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`. + """ + This class stores passwords using LDAP's "Salted SHA1" format, + and follows the :ref:`password-hash-api`. It supports a 4-16 byte salt. @@ -196,8 +200,91 @@ class ldap_salted_sha1(_SaltedBase64DigestHelper): ident = u("{SSHA}") checksum_size = 20 _hash_func = sha1 + # NOTE: 32 = ceil((20 + 4) * 4/3) _hash_regex = re.compile(u(r"^\{SSHA\}(?P<tmp>[+/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(u(r"^\{SSHA256\}(?P<tmp>[+/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(u(r"^\{SSHA512\}(?P<tmp>[+/a-zA-Z0-9]{91,}={0,2})$")) + + class ldap_plaintext(plaintext): """This class stores passwords in plaintext, and follows the :ref:`password-hash-api`. diff --git a/passlib/hash.py b/passlib/hash.py index 9b72448..2cc0628 100644 --- a/passlib/hash.py +++ b/passlib/hash.py @@ -43,7 +43,7 @@ if False: from passlib.handlers.digests import hex_md4, hex_md5, hex_sha1, hex_sha256, hex_sha512, htdigest from passlib.handlers.django import django_bcrypt, django_bcrypt_sha256, django_des_crypt, django_disabled, django_pbkdf2_sha1, django_pbkdf2_sha256, django_salted_md5, django_salted_sha1 from passlib.handlers.fshp import fshp - from passlib.handlers.ldap_digests import ldap_bcrypt, ldap_bsdi_crypt, ldap_des_crypt, ldap_md5, ldap_md5_crypt, ldap_plaintext, ldap_salted_md5, ldap_salted_sha1, ldap_sha1, ldap_sha1_crypt, ldap_sha256_crypt, ldap_sha512_crypt + from passlib.handlers.ldap_digests import ldap_bcrypt, ldap_bsdi_crypt, ldap_des_crypt, ldap_md5, ldap_md5_crypt, ldap_plaintext, ldap_salted_md5, ldap_salted_sha1, ldap_salted_sha256, ldap_salted_sha512, ldap_sha1, ldap_sha1_crypt, ldap_sha256_crypt, ldap_sha512_crypt from passlib.handlers.md5_crypt import apr_md5_crypt, md5_crypt from passlib.handlers.misc import plaintext, unix_disabled, unix_fallback from passlib.handlers.mssql import mssql2000, mssql2005 diff --git a/passlib/registry.py b/passlib/registry.py index 5c9cb6e..9964b25 100644 --- a/passlib/registry.py +++ b/passlib/registry.py @@ -125,6 +125,8 @@ _locations = dict( ldap_hex_sha1 = "passlib.handlers.roundup", ldap_salted_md5 = "passlib.handlers.ldap_digests", ldap_salted_sha1 = "passlib.handlers.ldap_digests", + ldap_salted_sha256 = "passlib.handlers.ldap_digests", + ldap_salted_sha512 = "passlib.handlers.ldap_digests", ldap_des_crypt = "passlib.handlers.ldap_digests", ldap_bsdi_crypt = "passlib.handlers.ldap_digests", ldap_md5_crypt = "passlib.handlers.ldap_digests", diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 9d81036..e7d2277 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -556,6 +556,67 @@ class ldap_salted_sha1_test(HandlerCase): '{SSHA}P90+qijSp8MJ1tN25j5o1PflUvlqjXHOGeOck===', ] + +class ldap_salted_sha256_test(HandlerCase): + handler = hash.ldap_salted_sha256 + known_correct_hashes = [ + # generated locally + # salt size = 8 + ("password", '{SSHA256}x1tymSTVjozxQ2PtT46ysrzhZxbcskK0o2f8hEFx7fAQQmhtDSEkJA=='), + ("test", '{SSHA256}xfqc9aOR6z15YaEk3/Ufd7UL9+JozB/1EPmCDTizL0GkdA7BuNda6w=='), + ("toomanysecrets", '{SSHA256}RrTKrg6HFXcjJ+eDAq4UtbODxOr9RLeG+I69FoJvutcbY0zpfU+p1Q=='), + (u('letm\xe8\xefn'), '{SSHA256}km7UjUTBZN8a+gf1ND2/qn15N7LsO/jmGYJXvyTfJKAbI0RoLWWslQ=='), + + # alternate salt sizes (4, 15, 16) + # generated locally + ('test', '{SSHA256}TFv2RpwyO0U9mA0Hk8FsXRa1I+4dNUtv27Qa8dzGVLinlDIm'), + ('test', '{SSHA256}J6MFQdkfjdmXz9UyUPb773kekJdm4dgSL4y8WQEQW11VipHSundOKaV0LsV4L6U='), + ('test', '{SSHA256}uBLazLaiBaPb6Cpnvq2XTYDkvXbYIuqRW1anMKk85d1/j1GqFQIgpHSOMUYIIcS4'), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SSHA256}Lpdyr1+lR+rtxgp3SpQnUuNw33ENivTl28nzF2ZI4Gm41/o=', + + # incorrect base64 encoding + '{SSHA256}TFv2RpwyO0U9mA0Hk8FsXRa1I+4dNUtv27Qa8dzGVLinlDI@', + '{SSHA256}TFv2RpwyO0U9mA0Hk8FsXRa1I+4dNUtv27Qa8dzGVLinlDI', + '{SSHA256}TFv2RpwyO0U9mA0Hk8FsXRa1I+4dNUtv27Qa8dzGVLinlDIm===', + ] + + + +class ldap_salted_sha512_test(HandlerCase): + handler = hash.ldap_salted_sha512 + known_correct_hashes = [ + # generated by testing ldap server web interface (see issue 124 comments) + # salt size = 8 + ("toomanysecrets", '{SSHA512}wExp4xjiCHS0zidJDC4UJq9EEeIebAQPJ1PWSwfhxWjfutI9XiiKuHm2AE41cEFfK+8HyI8bh+ztbczUGsvVFIgICWWPt7qu'), + (u('letm\xe8\xefn'), '{SSHA512}mpNUSmZc3TNx+RnPwkIAVMf7ocEKLPrIoQNsg4Eu8dHvyCeb2xzHp5A6n4tF7ntknSvfvRZaJII4ImvNJlYsgiwAm0FMqR+3'), + + # generated locally + # salt size = 8 + ("password", '{SSHA512}f/lFQskkl7PdMsTGJxHZq8LDt/l+UqRMm6/pj4pV7/xZkcOaKCgvQqp+KCeXc/Vd4RY6vEHWn4y0DnFcQ6wgyv9fyxk='), + ("test", '{SSHA512}Tgx/uhHnlM9/GgQvI31dN7cheDXg7WypZwaaIkyRsgV/BKIzBG3G/wUd9o1dpi06p3SYzMedg0lvTc3b6CtdO0Xo/f9/L+Uc'), + + # alternate salt sizes (4, 15, 16) + # generated locally + ('test', '{SSHA512}Yg9DQ2wURCFGwobu7R2O6cq7nVbnGMPrFCX0aPQ9kj/y1hd6k9PEzkgWCB5aXdPwPzNrVb0PkiHiBnG1CxFiT+B8L8U='), + ('test', '{SSHA512}5ecDGWs5RY4xLszUO6hAcl90W3wAozGQoI4Gqj8xSZdcfU1lVEM4aY8s+4xVeLitcn7BO8i7xkzMFWLoxas7SeHc23sP4dx77937PyeE0A=='), + ('test', '{SSHA512}6FQv5W47HGg2MFBFZofoiIbO8KRW75Pm51NKoInpthYQQ5ujazHGhVGzrj3JXgA7j0k+UNmkHdbJjdY5xcUHPzynFEII4fwfIySEcG5NKSU='), + ] + + known_malformed_hashes = [ + # salt too small (3) + '{SSHA512}zFnn4/8x8GveUaMqgrYWyIWqFQ0Irt6gADPtRk4Uv3nUC6uR5cD8+YdQni/0ZNij9etm6p17kSFuww3M6l+d6AbAeA==', + + # incorrect base64 encoding + '{SSHA512}Tgx/uhHnlM9/GgQvI31dN7cheDXg7WypZwaaIkyRsgV/BKIzBG3G/wUd9o1dpi06p3SYzMedg0lvTc3b6CtdO0Xo/f9/L+U', + '{SSHA512}Tgx/uhHnlM9/GgQvI31dN7cheDXg7WypZwaaIkyRsgV/BKIzBG3G/wUd9o1dpi06p3SYzMedg0lvTc3b6CtdO0Xo/f9/L+U@', + '{SSHA512}Tgx/uhHnlM9/GgQvI31dN7cheDXg7WypZwaaIkyRsgV/BKIzBG3G/wUd9o1dpi06p3SYzMedg0lvTc3b6CtdO0Xo/f9/L+U===', + ] + + class ldap_plaintext_test(HandlerCase): # TODO: integrate EncodingHandlerMixin handler = hash.ldap_plaintext |