summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-03-28 00:16:59 -0400
committerEli Collins <elic@assurancetechnologies.com>2011-03-28 00:16:59 -0400
commiteeb244787205ba1139ad8cf971bfd939af8bbbd0 (patch)
tree6367e10989703f8022a25f9c533ca84ea9706455
parent7c7bcf5d4eb3cc929bb7ab4411515dd042d0f0e5 (diff)
downloadpasslib-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.rst191
-rw-r--r--docs/lib/passlib.hash.rst1
-rw-r--r--docs/lib/passlib.utils.h64.rst2
-rw-r--r--passlib/handlers/pbkdf2.py319
-rw-r--r--passlib/registry.py5
-rw-r--r--passlib/tests/test_drivers.py73
-rw-r--r--passlib/utils/__init__.py35
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
#=================================================================================