diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-03-28 00:16:59 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-03-28 00:16:59 -0400 |
| commit | eeb244787205ba1139ad8cf971bfd939af8bbbd0 (patch) | |
| tree | 6367e10989703f8022a25f9c533ca84ea9706455 | |
| parent | 7c7bcf5d4eb3cc929bb7ab4411515dd042d0f0e5 (diff) | |
| download | passlib-eeb244787205ba1139ad8cf971bfd939af8bbbd0.tar.gz | |
added support for a bunch of PBKDF2 hash schemes
* pbkdf2_sha1, pbkdf2_sha256, pbkdf2_sha512 -- 3 custom schemes defined by passlib
* dlitz_pbkdf2_sha1 -- Dwayne Litzenberger's PBKDF2 crypt
* grub_pbkdf2_sha512 -- Grub2's PBKDF2 hash format
* two util support functions: adapted_b64_(encode|decode)
* UTs and docs for all of the above
| -rw-r--r-- | docs/lib/passlib.hash.pbkdf2_digests.rst | 191 | ||||
| -rw-r--r-- | docs/lib/passlib.hash.rst | 1 | ||||
| -rw-r--r-- | docs/lib/passlib.utils.h64.rst | 2 | ||||
| -rw-r--r-- | passlib/handlers/pbkdf2.py | 319 | ||||
| -rw-r--r-- | passlib/registry.py | 5 | ||||
| -rw-r--r-- | passlib/tests/test_drivers.py | 73 | ||||
| -rw-r--r-- | passlib/utils/__init__.py | 35 |
7 files changed, 626 insertions, 0 deletions
diff --git a/docs/lib/passlib.hash.pbkdf2_digests.rst b/docs/lib/passlib.hash.pbkdf2_digests.rst new file mode 100644 index 0000000..d2906b5 --- /dev/null +++ b/docs/lib/passlib.hash.pbkdf2_digests.rst @@ -0,0 +1,191 @@ +========================================================== +:samp:`passlib.hash.pbkdf2_{digest}` - PBKDF2-based Hashes +========================================================== + +.. index:: pbkdf2; password hashes, grub, grub; grub-mkpasswd-pbkdf2 + +.. currentmodule:: passlib.hash + +Overview +======== +PassLib provides support for a number of hashes based +on the PBKDF2 [#pbkdf2]_ algorithm. PBKDF2 is ideally +suited as the basis for a password hash, as it provides +variable length salts, variable number of rounds, +and (in combination with HMAC) can be tailored +to use almost any cryptographic hash as the basis +for it's operation. + +PassLib supports 5 PBKDF2-based hash schemes: + +* PassLib's PBKDF2 hashes -- :class:`!pbkdf2_sha1`, + :class:`!pbkdf2_sha256`, :class:`!pbkdf2_sha512` -- + are three custom schemes defined and provided by PassLib. + They have a straightforward implementation, + and a format almost identical to that of :class:`sha512_crypt`. + Thus, while they are currently only implemented within PassLib, + they should be secure and extremely portable. + +* :class:`!dlitz_pbkdf2_sha1` provides an implementation of Dwayne Litzenberger's + PBKDF2-HMAC-SHA1 hash format [#dlitz]_. + +* :class:`!grub_pbkdf2_sha512` provides an implementation of Grub's PBKDF2-HMAC-SHA512 + password hash [#grub]_, as generated by the :command:`grub-mkpasswd-pbkdf2` command, + and may be found in Grub2 configuration files. + +Usage +===== +These classes support both rounds and salts, +and can be used in the exact same manner +as :doc:`SHA-512 Crypt <passlib.hash.sha512_crypt>`. + +Interface +========= +.. autoclass:: pbkdf2_sha1() +.. autoclass:: pbkdf2_sha256() +.. autoclass:: pbkdf2_sha512() +.. autoclass:: dlitz_pbkdf2_sha1() +.. autoclass:: grub_pbkdf2_sha512() + +.. rst-class:: html-toggle + +Format & Algorithm +================== + +Passlib's PBKDF2 Hashes +----------------------- +:class:`!pbkdf2_sha1`, :class:`!pbkdf2_sha256`, :class:`!pbkdf2_sha512` + + An example :class:`!pbkdf2_sha256` hash (of ``password``): + + ``$pbkdf2-sha256$6400$.6UI/S.nXIk8jcbdHx3Fhg$98jZicV16ODfEsEZeYPGHU3kbrUrvUEXOPimVSQDD44``. + + All of the pbkdf2 hashes defined by passlib + follow the same format, :samp:`$pbkdf2-{digest}${rounds}${salt}${checksum}`. + + * :samp:`$pbkdf2-{digest}$`` is used as the :ref:`modular-crypt-format` identifier + (``$pbkdf2-sha256$`` in the example). + + * :samp:`{digest}` - this specifies the particular cryptographic hash + used in conjunction with HMAC to form PBKDF2's pseudorandom function + for that particular hash (``sha256`` in the example). + + * :samp:`{rounds}` - the number of iterations that should be performed. + this is encoded as a positive decimal number with no zero-padding + (``6400`` in the example). + + * :samp:`{salt}` - this is the :func:`adapted base64 encoding <passlib.utils.adapted_b64_encode>` + of the raw salt bytes passed into the PBKDF2 function. + + * :samp:`{checksum}` - this is the :func:`adapted base64 encoding <passlib.utils.adapted_b64_encode>` + of the raw derived key bytes returned from the PBKDF2 function. + Each scheme uses output size of it's specific :samp:`{digest}` + as the size of the raw derived key. This is enlarged + by appromixately 4/3 by the base64 encoding, + resulting in a checksum size of 27, 43, and 86 for each of the respective algorithms. + + The algorithm used by all of these schemes is deliberately identical and simple: + The password is encoded into UTF-8 if not already encoded, + and passed through :func:`~passlib.utils.pbkdf2.pbkdf2` + along with the decoded salt, and the number of rounds. + The result is then encoded using :func:`~passlib.utils.adapted_b64_encode`. + + .. note:: + + The base64 encoding used by these functions uses the same + :mod:`hash64 <passlib.utils.h64>` character set as most + other Unix password hashes, but uses the standard base64 encoding scheme, + instead of hash64's alternate value mapping. + + This was done deliberately to create a simple implementation using + common components, which none-the-less uses the same character set + as existing :ref:`modular-crypt-format` hashes, so it can be + used the same contexts without breaking charset expectations. + +Other PBKDF2 Hashes +------------------- +:class:`!dlitz_pbkdf2_sha1` + + A example hash (of ``password``) is: + + ``$p5k2$2710$.pPqsEwHD7MiECU0$b8TQ5AMQemtlaSgegw5Je.JBE3QQhLbO``. + + All of this scheme's hashes have the format :samp:`$p5k2${rounds}${salt}${checksum}`, + where: + + * ``$p5k2$`` is used as the :ref:`modular-crypt-format` identifier. + + * :samp:`{rounds}` is the number of PBKDF2 iterations to perform, + stored as lowercase hexidecimal number with no zero-padding (in the example: ``2710`` or 10000 iterations). + + * :samp:`{salt}` is the salt string, which can be any number of characters, + drawn from the :ref:`hash64 charset <h64charset>` + (``.pPqsEwHD7MiECU0`` in the example). + + * :samp:`{checksum}` is 32 characters, which encode + the resulting 24-byte PBKDF2 derived key using :func:`~passlib.utils.adapted_b64_encode` + (``b8TQ5AMQemtlaSgegw5Je.JBE3QQhLbO`` in the example). + + In order to generate the checksum, the password is first encoded into UTF-8 if it's unicode. + Then, the entire configuration string (all of the hash except the checksum, ie :samp:`$p5k2${rounds}${salt}`) + is used as the PBKDF2 salt. PBKDF2 is called using the encoded password, the full salt, + the specified number of rounds, and using HMAC-SHA1 as it's psuedorandom function. + 24 bytes of derived key are requested, and the resulting key is encoded and used + as the checksum portion of the hash. + +:class:`!grub_pbkdf2_sha512` + + A example hash (of ``password``) is :: + + grub.pbkdf2.sha512.10000.4483972AD2C52E1F590B3E2260795FDA9CA0B07B + 96FF492814CA9775F08C4B59CD1707F10B269E09B61B1E2D11729BCA8D62B7827 + B25B093EC58C4C1EAC23137.DF4FCB5DD91340D6D31E33423E4210AD47C7A4DF9 + FA16F401663BF288C20BF973530866178FE6D134256E4DBEFBD984B652332EED3 + ACAED834FEA7B73CAE851D + + All of this scheme's hashes have the format :samp:`grub.pbkdf2.sha512.{rounds}.{salt}.{checksum}`, + where :samp:`{rounds}` is the number of iteration stored in decimal, + :samp:`{salt}` is the salt string encoded using upper-case hexdecimal, + and :samp:`{checksum}` is the resulting 64-byte derived key, also + encoded in upper-case hexidecimal. It can be identified by the prefix ``grub.pdkdf2.sha512.``. + + The algorithm used is the same as :class:`pbkdf2_sha1`: the password is encoded into UTF-8 if not already encoded, + and passed through :func:`~passlib.utils.pbkdf2.pbkdf2` + along with the decoded salt, and the number of rounds. + The result is then encoded into hexidecimal. + +Hash Translation +---------------- +Note that despite encoding and format differences, +:class:`!pbkdf2_sha512` and :class:`!grub_pbkdf2_sha512` share an identical algorithm, +and one can be converted to the other using the following code:: + + >>> from passlib.hash import pbkdf2_sha512, grub_pbkdf2_sha512 + + >>> #given a pbkdf2_sha512 hash... + >>> h = pbkdf2_sha512.encrypt("password") + >>> h + '$pbkdf2-sha512$6400$y6vYff3SihJiqumIrNXwGw$NobVwyUlVI52/Cvrguwli5fX6XgKHNUf7fWWS2VgoWEevaTCiZx4OCYhwGFwzUAuz/g1zQVSIf.9JEb0BEVEEA' + + >>> #it can be parsed into options + >>> hobj = pbkdf2_sha512.from_string(h) + >>> rounds, salt, chk = hobj.rounds, hobj.salt, hobj.checksum + + >>> #and a new grub hash can be created + >>> gobj = grub_pbkdf2_sha512(rounds=rounds, salt=salt, checksum=chk) + >>> g = gobj.to_string() + >>> g + + >>> grub_pbkdf2_sha512.verify("password", g) + True + +References +========== + +.. [#hmac-sha1] While SHA1 has fallen to collision attacks, HMAC-SHA1 is still considered secure - `<http://www.schneier.com/blog/archives/2005/02/sha1_broken.html>`_. + +.. [#pbkdf2] The specification for the PBKDF2 algorithm - `<http://tools.ietf.org/html/rfc2898#section-5.2>`_. + +.. [#dlitz] Dwayne C. Litzenberger's PBKDF2 hash - `<http://www.dlitz.net/software/python-pbkdf2/>`_. + +.. [#grub] Information about Grub's password hashes - `<http://grub.enbug.org/Authentication>`_. diff --git a/docs/lib/passlib.hash.rst b/docs/lib/passlib.hash.rst index 27bcb4b..6f70689 100644 --- a/docs/lib/passlib.hash.rst +++ b/docs/lib/passlib.hash.rst @@ -64,6 +64,7 @@ the modular crypt format. passlib.hash.apr_md5_crypt passlib.hash.phpass passlib.hash.nthash + passlib.hash.pbkdf2_digests Database Schemes ---------------- diff --git a/docs/lib/passlib.utils.h64.rst b/docs/lib/passlib.utils.h64.rst index 5ba5b6e..89c5bde 100644 --- a/docs/lib/passlib.utils.h64.rst +++ b/docs/lib/passlib.utils.h64.rst @@ -22,6 +22,8 @@ and decoding strings in that format. when in fact bcrypt uses yet another ordering, which does not match hash64 or other base64 schemes. +.. _h64charset: + Constants ========= .. data:: CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" diff --git a/passlib/handlers/pbkdf2.py b/passlib/handlers/pbkdf2.py new file mode 100644 index 0000000..5b0426d --- /dev/null +++ b/passlib/handlers/pbkdf2.py @@ -0,0 +1,319 @@ +"""passlib.handlers.pbkdf - PBKDF2 based hashes""" +#========================================================= +#imports +#========================================================= +#core +from binascii import hexlify, unhexlify +from base64 import b64encode +import re +import logging; log = logging.getLogger(__name__) +from warnings import warn +#site +#libs +from passlib.utils import adapted_b64_encode, adapted_b64_decode, ALL_BYTE_VALUES +from passlib.utils.handlers import ExtendedHandler +from passlib.utils.pbkdf2 import pbkdf2 +#pkg +#local +__all__ = [ + "pbkdf2_sha1", + "pbkdf2_sha256", + "pbkdf2_sha512", + "dlitz_pbkdf2_sha1", + "grub_pbkdf2_sha512", +] + +#========================================================= +# +#========================================================= +class Pbkdf2DigestHandler(ExtendedHandler): + "base class for various pbkdf2_{digest} algorithms" + setting_kwds = ("salt", "rounds") + + _ident = None #subclass specified identifier prefix + _prf = None #subclass specified prf identifier + + #NOTE: max_salt_chars and max_rounds are arbitrarily chosen to provide sanity check. + # the underlying pbkdf2 specifies no bounds for either. + + #NOTE: defaults chosen to be at least as large as pbkdf2 rfc recommends... + # >8 bytes of entropy in salt, >1000 rounds + # increased due to time since rfc established + + default_salt_chars = 16 + min_salt_chars = 0 + max_salt_chars = 1024 + salt_charset = ALL_BYTE_VALUES + + default_rounds = 6400 + min_rounds = 1 + max_rounds = 2**32-1 + rounds_cost = "linear" + + @classmethod + def identify(cls, hash): + return bool(hash) and hash.startswith(cls._ident) + + @classmethod + def from_string(cls, hash): + if not hash: + raise ValueError, "no hash specified" + ident = cls._ident + if not hash.startswith(ident): + raise ValueError, "invalid %s hash" % (cls.name,) + parts = hash[len(ident):].split("$") + if len(parts) == 3: + rounds, salt, chk = parts + elif len(parts) == 2: + rounds, salt = parts + chk = None + else: + raise ValueError, "invalid %s hash" % (cls.name,) + int_rounds = int(rounds) + if rounds != str(int_rounds): #forbid zero padding, etc. + raise ValueError, "invalid %s hash" % (cls.name,) + raw_salt = adapted_b64_decode(salt) + raw_chk = adapted_b64_decode(chk) if chk else None + return cls( + rounds=int_rounds, + salt=raw_salt, + checksum=raw_chk, + strict=bool(raw_chk), + ) + + def to_string(self, withchk=True): + salt = adapted_b64_encode(self.salt) + if withchk and self.checksum: + return '%s%d$%s$%s' % (self._ident, self.rounds, salt, adapted_b64_encode(self.checksum)) + else: + return '%s%d$%s' % (self._ident, self.rounds, salt) + + def calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return pbkdf2(secret, self.salt, self.rounds, self.checksum_chars, self._prf) + +def create_pbkdf2_hash(hash_name, digest_size): + "create new Pbkdf2DigestHandler subclass for a specific hash" + name = 'pbkdf2_' + hash_name + ident = "$pbkdf2-%s$" % (hash_name,) + prf = "hmac-%s" % (hash_name,) + base = Pbkdf2DigestHandler + return type(name, (base,), dict( + name=name, + _ident=ident, + _prf = prf, + checksum_chars=digest_size, + encoded_checksum_chars=(digest_size*4+2)//3, + __doc__="""This class implements passlib's pbkdf2-%(prf)s hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + + :param salt: + Optional salt bytes. + If specified, the length must be between 0-1024 bytes. + If not specified, a %(dsc)d byte salt will be autogenerated (this is recommended). + + :param rounds: + Optional number of rounds to use. + Defaults to %(dr)d, but must be within ``range(1,1<<32)``. + """ % dict(prf=prf, dsc=base.default_salt_chars, dr=base.default_rounds) + )) + +#--------------------------------------------------------- +#derived handlers +#--------------------------------------------------------- +pbkdf2_sha1 = create_pbkdf2_hash("sha1", 20) +pbkdf2_sha256 = create_pbkdf2_hash("sha256", 32) +pbkdf2_sha512 = create_pbkdf2_hash("sha512", 64) + +#========================================================= +#dlitz's pbkdf2 hash +#========================================================= +class dlitz_pbkdf2_sha1(ExtendedHandler): + """This class implements Dwayne Litzenberger's PBKDF2-based crypt algorithm, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + + :param salt: + Optional salt string. + If specified, it may be any length, but must use the characters in the regexp range ``[./0-9A-Za-z]``. + If not specified, a 16 character salt will be autogenerated (this is recommended). + + :param rounds: + Optional number of rounds to use. + Defaults to 10000, must be within ``range(1,1<<32)``. + """ + + #========================================================= + #class attrs + #========================================================= + name = "dlitz_pbkdf2_sha1" + setting_kwds = ("salt", "rounds") + + #NOTE: max_salt_chars and max_rounds are arbitrarily chosen to provide sanity check. + # underlying algorithm (and reference implementation) allow effectively unbounded values for both of these. + + default_salt_chars = 16 + min_salt_chars = 0 + max_salt_chars = 1024 + + default_rounds = 10000 + min_rounds = 0 + max_rounds = 2**32-1 + rounds_cost = "linear" + + #========================================================= + #formatting + #========================================================= + + @classmethod + def identify(cls, hash): + return bool(hash) and hash.startswith("$p5k2$") + + #hash $p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g + #ident $p5k2$ + #rounds c + #salt u9HvcT4d + #chk Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g + #rounds in lowercase hex, no zero padding + + _pat = re.compile(r""" + ^ + \$p5k2 + \$(?P<rounds>[a-f0-9]*) + \$(?P<salt>[A-Za-z0-9./]*) + (\$(?P<chk>[A-Za-z0-9./]{32}))? + $ + """, re.X) + + @classmethod + def from_string(cls, hash): + if not hash: + raise ValueError, "no hash specified" + m = cls._pat.match(hash) + if not m: + raise ValueError, "invalid dlitz_pbkdf2_crypt hash" + rounds, salt, chk = m.group("rounds", "salt", "chk") + if rounds.startswith("0"): #zero not allowed, nor left-padded with zeroes + raise ValueError, "invalid dlitz_pbkdf2_crypt hash" + rounds = int(rounds, 16) if rounds else 400 + return cls( + rounds=rounds, + salt=salt, + checksum=chk, + strict=bool(chk), + ) + + def to_string(self, withchk=True): + if self.rounds == 400: + out = '$p5k2$$%s' % (self.salt,) + else: + out = '$p5k2$%x$%s' % (self.rounds, self.salt) + if withchk and self.checksum: + out = "%s$%s" % (out,self.checksum) + return out + + #========================================================= + #backend + #========================================================= + def calc_checksum(self, secret): + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + salt = self.to_string(withchk=False) + result = pbkdf2(secret, salt, self.rounds, 24, "hmac-sha1") + return adapted_b64_encode(result) + + #========================================================= + #eoc + #========================================================= + +#========================================================= +#grub +#========================================================= +class grub_pbkdf2_sha512(ExtendedHandler): + """This class implements Grub's pbkdf2-hmac-sha512 hash, and follows the :ref:`password-hash-api`. + + It supports a variable-length salt, and a variable number of rounds. + + The :meth:`encrypt()` and :meth:`genconfig` methods accept the following optional keywords: + + :param salt: + Optional salt bytes. + If specified, the length must be between 0-1024 bytes. + If not specified, a 64 byte salt will be autogenerated (this is recommended). + + :param rounds: + Optional number of rounds to use. + Defaults to 10000, but must be within ``range(1,1<<32)``. + """ + name = "grub_pbkdf2_sha512" + setting_kwds = ("salt", "rounds") + + _ident = "grub.pbkdf2.sha512." + + #NOTE: max_salt_chars and max_rounds are arbitrarily chosen to provide sanity check. + # the underlying pbkdf2 specifies no bounds for either, + # and it's not clear what grub specifies. + + default_salt_chars = 64 + min_salt_chars = 0 + max_salt_chars = 1024 + salt_charset = ALL_BYTE_VALUES + + default_rounds = 10000 + min_rounds = 1 + max_rounds = 2**32-1 + rounds_cost = "linear" + + @classmethod + def identify(cls, hash): + return bool(hash) and hash.startswith(cls._ident) + + @classmethod + def from_string(cls, hash): + if not hash: + raise ValueError, "no hash specified" + ident = cls._ident + if not hash.startswith(ident): + raise ValueError, "invalid %s hash" % (cls.name,) + parts = hash[len(ident):].split(".") + if len(parts) == 3: + rounds, salt, chk = parts + elif len(parts) == 2: + rounds, salt = parts + chk = None + else: + raise ValueError, "invalid %s hash" % (cls.name,) + int_rounds = int(rounds) + if rounds != str(int_rounds): #forbid zero padding, etc. + raise ValueError, "invalid %s hash" % (cls.name,) + raw_salt = unhexlify(salt) + raw_chk = unhexlify(chk) if chk else None + return cls( + rounds=int_rounds, + salt=raw_salt, + checksum=raw_chk, + strict=bool(raw_chk), + ) + + def to_string(self, withchk=True): + salt = hexlify(self.salt).upper() + if withchk and self.checksum: + return '%s%d.%s.%s' % (self._ident, self.rounds, salt, hexlify(self.checksum).upper()) + else: + return '%s%d.%s' % (self._ident, self.rounds, salt) + + def calc_checksum(self, secret): + #TODO: find out what grub's policy is re: unicode + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + return pbkdf2(secret, self.salt, self.rounds, 64, "hmac-sha512") + +#========================================================= +#eof +#========================================================= diff --git a/passlib/registry.py b/passlib/registry.py index 6544f0c..6cbad47 100644 --- a/passlib/registry.py +++ b/passlib/registry.py @@ -79,6 +79,8 @@ _handler_locations = { "bsdi_crypt": ("passlib.handlers.des_crypt", "bsdi_crypt"), "crypt16": ("passlib.handlers.des_crypt", "crypt16"), "des_crypt": ("passlib.handlers.des_crypt", "des_crypt"), + "dlitz_pbkdf2_sha1":("passlib.handlers.pbkdf2", "dlitz_pbkdf2_sha1"), + "grub_pbkdf2_sha512":("passlib.handlers.pbkdf2", "grub_pbkdf2_sha512"), "hex_md4": ("passlib.handlers.digests", "hex_md4"), "hex_md5": ("passlib.handlers.digests", "hex_md5"), "hex_sha1": ("passlib.handlers.digests", "hex_sha1"), @@ -95,6 +97,9 @@ _handler_locations = { "nthash": ("passlib.handlers.nthash", "nthash"), "oracle10": ("passlib.handlers.oracle", "oracle10"), "oracle11": ("passlib.handlers.oracle", "oracle11"), + "pbkdf2_sha1": ("passlib.handlers.pbkdf2", "pbkdf2_sha1"), + "pbkdf2_sha256": ("passlib.handlers.pbkdf2", "pbkdf2_sha256"), + "pbkdf2_sha512": ("passlib.handlers.pbkdf2", "pbkdf2_sha512"), "phpass": ("passlib.handlers.phpass", "phpass"), "plaintext": ("passlib.handlers.misc", "plaintext"), "postgres_md5": ("passlib.handlers.postgres", "postgres_md5"), diff --git a/passlib/tests/test_drivers.py b/passlib/tests/test_drivers.py index 3c9ce67..f55ef13 100644 --- a/passlib/tests/test_drivers.py +++ b/passlib/tests/test_drivers.py @@ -412,6 +412,79 @@ class Oracle11Test(HandlerCase): ] #========================================================= +#pbkdf2 hashes +#========================================================= +from passlib.handlers import pbkdf2 as pk2 + +class Pbkdf2Sha1Test(HandlerCase): + handler = pk2.pbkdf2_sha1 + known_correct_hashes = ( + ("password", '$pbkdf2-sha1$1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI'), + (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', + '$pbkdf2-sha1$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc'), + ) + +class Pbkdf2Sha256Test(HandlerCase): + handler = pk2.pbkdf2_sha256 + known_correct_hashes = ( + ("password", + '$pbkdf2-sha256$1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg.fJPeq1h/gXXY7acBp9/6c.tmQ' + ), + (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', + '$pbkdf2-sha256$1212$3SABFJGDtyhrQMVt1uABPw$WyaUoqCLgvz97s523nF4iuOqZNbp5Nt8do/cuaa7AiI' + ), + ) + +class Pbkdf2Sha512Test(HandlerCase): + handler = pk2.pbkdf2_sha512 + known_correct_hashes = ( + ("password", + '$pbkdf2-sha512$1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1' + '7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww' + ), + (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', + '$pbkdf2-sha512$1212$KkbvoKGsAIcF8IslDR6skQ$8be/PRmd88Ps8fmPowCJt' + 'tH9G3vgxpG.Krjt3KT.NP6cKJ0V4Prarqf.HBwz0dCkJ6xgWnSj2ynXSV7MlvMa8Q' + ), + ) + +class DlitzPbkdf2Sha1Test(HandlerCase): + handler = pk2.dlitz_pbkdf2_sha1 + known_correct_hashes = ( + #test vectors from original implementation + ('cloadm', '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'), + ('gnu', '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'), + ('dcl', '$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL'), + ('spam', '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'), + (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', + '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'), + ) + +class GrubPbkdf2Sha512Test(HandlerCase): + handler = pk2.grub_pbkdf2_sha512 + known_correct_hashes = ( + #test vectors generated from cmd line tool + + #salt=32 bytes + (u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', + 'grub.pbkdf2.sha512.10000.BCAC1CEC5E4341C8C511C529' + '7FA877BE91C2817B32A35A3ECF5CA6B8B257F751.6968526A' + '2A5B1AEEE0A29A9E057336B48D388FFB3F600233237223C21' + '04DE1752CEC35B0DD1ED49563398A282C0F471099C2803FBA' + '47C7919CABC43192C68F60'), + + #salt=64 bytes + ('toomanysecrets', + 'grub.pbkdf2.sha512.10000.9B436BB6978682363D5C449B' + 'BEAB322676946C632208BC1294D51F47174A9A3B04A7E4785' + '986CD4EA7470FAB8FE9F6BD522D1FC6C51109A8596FB7AD48' + '7C4493.0FE5EF169AFFCB67D86E2581B1E251D88C777B98BA' + '2D3256ECC9F765D84956FC5CA5C4B6FD711AA285F0A04DCF4' + '634083F9A20F4B6F339A52FBD6BED618E527B'), + + ) + +#========================================================= #PHPass Portable Crypt #========================================================= from passlib.handlers.phpass import phpass diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py index 0d9570d..d387602 100644 --- a/passlib/utils/__init__.py +++ b/passlib/utils/__init__.py @@ -3,6 +3,7 @@ #imports #================================================================================= #core +from base64 import b64encode, b64decode from cStringIO import StringIO from functools import update_wrapper from hashlib import sha256 @@ -316,6 +317,40 @@ def xor_bytes(left, right): return _join(chr(ord(l) ^ ord(r)) for l, r in zip(left, right)) #================================================================================= +#alt base64 encoding +#================================================================================= + +def adapted_b64_encode(data): + """encode using variant of base64 + + the output of this function is identical to b64_encode, + except that it uses ``.`` instead of ``+``, + and omits trailing padding ``=`` and whitepsace. + + it is primarily used for by passlib's custom pbkdf2 hashes. + """ + return b64encode(data, "./").strip("=\n") + +def adapted_b64_decode(data, sixthree="."): + """decode using variant of base64 + + the input of this function is identical to b64_decode, + except that it uses ``.`` instead of ``+``, + and should not include trailing padding ``=`` or whitespace. + + it is primarily used for by passlib's custom pbkdf2 hashes. + """ + off = len(data) % 4 + if off == 0: + return b64decode(data, "./") + elif off == 1: + raise ValueError, "invalid bas64 input" + elif off == 2: + return b64decode(data + "==", "./") + else: + return b64decode(data + "=", "./") + +#================================================================================= #randomness #================================================================================= |
