summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-03-09 18:45:34 -0500
committerEli Collins <elic@assurancetechnologies.com>2012-03-09 18:45:34 -0500
commit72ec6bedc4fdad8845b39788430f32345234ca67 (patch)
treecfd206065093a58646f6b4bef9415804b6c0d96a
parente0803178ae49f1fbaa367a7564b9877eacce628e (diff)
downloadpasslib-72ec6bedc4fdad8845b39788430f32345234ca67.tar.gz
utils.handlers framework reworked; removed a bunch of boilerplate code
* StaticHandler is now subclass of GenericHandler - _calc_checksum() should be implemented instead of encrypt(). (compatibility stub added so old code should continue to work) - _norm_hash() no longer needs to handle ->unicode conversion - default from_string() contains a bunch of features, including stripping a known prefix, etc. * context kwds now pulled into constructor, so GenericHandler supports context kwds properly; HasUserContext mixin added to support common 'user' context kwd * identify_regexp & identify_prefix removed, functionality rolled into default GenericHandler.identify() implementation. - default identify checks _hash_regex as potential way to identify hashes * HasStubChecksum removed, functionality rolled into GenericHandler * HasRawChecksum now just sets a flag, functionality moved into GenericHandler * HasManyIdents._parse_ident() helper added to valid & split identifier from hashes. * GenericHandler._norm_checksum() is now strict about unicode / bytes
-rw-r--r--CHANGES11
-rw-r--r--docs/lib/passlib.utils.handlers.rst11
-rw-r--r--passlib/handlers/bcrypt.py16
-rw-r--r--passlib/handlers/des_crypt.py43
-rw-r--r--passlib/handlers/digests.py44
-rw-r--r--passlib/handlers/django.py26
-rw-r--r--passlib/handlers/fshp.py18
-rw-r--r--passlib/handlers/ldap_digests.py66
-rw-r--r--passlib/handlers/misc.py65
-rw-r--r--passlib/handlers/mysql.py68
-rw-r--r--passlib/handlers/nthash.py13
-rw-r--r--passlib/handlers/oracle.py65
-rw-r--r--passlib/handlers/pbkdf2.py6
-rw-r--r--passlib/handlers/phpass.py11
-rw-r--r--passlib/handlers/postgres.py39
-rw-r--r--passlib/handlers/sha2_crypt.py12
-rw-r--r--passlib/handlers/sun_md5_crypt.py11
-rw-r--r--passlib/tests/test_context.py5
-rw-r--r--passlib/tests/test_utils_handlers.py118
-rw-r--r--passlib/utils/handlers.py622
20 files changed, 627 insertions, 643 deletions
diff --git a/CHANGES b/CHANGES
index e934b7a..b558976 100644
--- a/CHANGES
+++ b/CHANGES
@@ -46,6 +46,10 @@ Release History
.. currentmodule:: passlib.utils.handlers
+ * Internal handler framework (:mod:`passlib.utils.handlers`) rewritten
+ drastically. Provides stricter input checking, reduction in
+ boilerplate code.
+
* :class:`~passlib.utils.handlers.GenericHandler` and related mixins
changed in backward-incompatible way: the ``strict`` keyword
was removed. :class:`!GenericHandler` now defaults to a behavior
@@ -54,6 +58,10 @@ Release History
The new keywords ``use_defaults`` and ``relaxed`` can be used
to disable these two requirements, respectively.
+ * :class:`~passlib.utils.handlers.StaticHandler` now derived from
+ :class:`!GenericHandler`, and required ``_calc_checksum()`` be
+ implemented instead of ``encrypt()``.
+
* :class:`~passlib.utils.handlers.GenericHandler` and related mixins
changed in backward-incompatible way: the :samp:`norm_{xxx}`
classmethods have been renamed to :samp:`_norm_{xxx}`, and turned
@@ -85,6 +93,9 @@ Release History
* Passlib is now source-compatible with Python 2.5+ and Python 3,
and no longer requires the use of :command:`2to3` to run under Python 3.
+ * Hash unittest framework rewritten. More border cases handled,
+ some simple fuzz testing added.
+
.. currentmodule:: passlib.hash
.. _consteq-issue:
diff --git a/docs/lib/passlib.utils.handlers.rst b/docs/lib/passlib.utils.handlers.rst
index 325c730..8d0e202 100644
--- a/docs/lib/passlib.utils.handlers.rst
+++ b/docs/lib/passlib.utils.handlers.rst
@@ -8,17 +8,18 @@
.. module:: passlib.utils.handlers
:synopsis: helper classes for writing password hash handlers
+.. warning::
+
+ This module is primarily used as an internal support module.
+ It's interface has not been finalized yet, and may change between major
+ releases of Passlib.
+
.. todo::
This module, and the instructions on how to write a custom handler,
definitely need to be rewritten for clarity. They are not yet
organized, and may leave out some important details.
-.. note::
-
- Since this module is primarily a support module used internally
- by Passlib, it's interface may change slightly between major releases.
-
Implementing Custom Handlers
============================
All that is required in order to write a custom handler that will work with
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py
index 5f89b66..0ca17f5 100644
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -122,16 +122,8 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
@classmethod
def from_string(cls, hash):
- if not hash:
- raise ValueError("no hash specified")
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
- for ident in cls.ident_values:
- if hash.startswith(ident):
- break
- else:
- raise ValueError("invalid bcrypt hash")
- rounds, data = hash[len(ident):].split(u("$"))
+ ident, tail = cls._parse_ident(hash)
+ rounds, data = tail.split(u("$"))
rval = int(rounds)
if rounds != u('%02d') % (rval,):
raise ValueError("invalid bcrypt hash (rounds not zero-padded)")
@@ -259,6 +251,10 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
# py2: unicode secret/hash encoded as ascii bytes before use,
# bytes takes as-is; returns ascii bytes.
# py3: can't get to install
+
+ # FIXME: bcryptor doesn't support v0 hashes ("$2$"),
+ # will throw bcryptor.engine.SaltError at this point.
+
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
hash = bcryptor_engine(False).hash_key(secret, self.to_string())
diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py
index 6b3c436..3eea448 100644
--- a/passlib/handlers/des_crypt.py
+++ b/passlib/handlers/des_crypt.py
@@ -174,17 +174,13 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
#=========================================================
#FORMAT: 2 chars of H64-encoded salt + 11 chars of H64-encoded checksum
- _pat = re.compile(u(r"""
+ _hash_regex = re.compile(u(r"""
^
(?P<salt>[./a-z0-9]{2})
(?P<chk>[./a-z0-9]{11})?
$"""), re.X|re.I)
@classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
-
- @classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
@@ -235,10 +231,6 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
#handler
#=========================================================
-#FIXME: phpass code notes that even rounds values should be avoided for BSDI-Crypt,
-# so as not to reveal weak des keys. given the random salt, this shouldn't be
-# a very likely issue anyways, but should do something about default rounds generation anyways.
-
class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
"""This class implements the BSDi-Crypt password hash, and follows the :ref:`password-hash-api`.
@@ -287,7 +279,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
#=========================================================
#internal helpers
#=========================================================
- _pat = re.compile(u(r"""
+ _hash_regex = re.compile(u(r"""
^
_
(?P<rounds>[./a-z0-9]{4})
@@ -296,16 +288,12 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
$"""), re.X|re.I)
@classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
-
- @classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
if isinstance(hash, bytes):
hash = hash.decode("ascii")
- m = cls._pat.match(hash)
+ m = cls._hash_regex.match(hash)
if not m:
raise ValueError("invalid ext-des-crypt hash")
rounds, salt, chk = m.group("rounds", "salt", "chk")
@@ -378,30 +366,19 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler):
#=========================================================
#internal helpers
#=========================================================
- _pat = re.compile(u(r"""
+ _hash_regex = re.compile(u(r"""
^
(?P<salt>[./a-z0-9]{2})
- (?P<chk>[./a-z0-9]{11,})?
+ (?P<chk>([./a-z0-9]{11})+)?
$"""), re.X|re.I)
@classmethod
- def identify(cls, hash):
- if not hash:
- return False
- if isinstance(hash, bytes):
- try:
- hash = hash.decode("ascii")
- except UnicodeDecodeError:
- return False
- return bool(cls._pat.match(hash)) and (len(hash)-2) % 11 == 0
-
- @classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
if isinstance(hash, bytes):
hash = hash.decode("ascii")
- m = cls._pat.match(hash)
+ m = cls._hash_regex.match(hash)
if not m:
raise ValueError("invalid bigcrypt hash")
salt, chk = m.group("salt", "chk")
@@ -469,23 +446,19 @@ class crypt16(uh.HasSalt, uh.GenericHandler):
#=========================================================
#internal helpers
#=========================================================
- _pat = re.compile(u(r"""
+ _hash_regex = re.compile(u(r"""
^
(?P<salt>[./a-z0-9]{2})
(?P<chk>[./a-z0-9]{22})?
$"""), re.X|re.I)
@classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
-
- @classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
if isinstance(hash, bytes):
hash = hash.decode("ascii")
- m = cls._pat.match(hash)
+ m = cls._hash_regex.match(hash)
if not m:
raise ValueError("invalid crypt16 hash")
salt, chk = m.group("salt", "chk")
diff --git a/passlib/handlers/digests.py b/passlib/handlers/digests.py
index 80b4371..03db62c 100644
--- a/passlib/handlers/digests.py
+++ b/passlib/handlers/digests.py
@@ -9,8 +9,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import to_native_str
-from passlib.utils.compat import bascii_to_str, bytes, unicode
+from passlib.utils import to_native_str, to_bytes
+from passlib.utils.compat import bascii_to_str, bytes, unicode, str_to_uascii
import passlib.utils.handlers as uh
from passlib.utils.md4 import md4
#pkg
@@ -29,35 +29,27 @@ __all__ = [
#=========================================================
class HexDigestHash(uh.StaticHandler):
"this provides a template for supporting passwords stored as plain hexidecimal hashes"
- _hash_func = None #required - hash function
- checksum_size = None #required - size of encoded digest
+ #=========================================================
+ # class attrs
+ #=========================================================
+ _hash_func = None # hash function to use - filled in by create_hex_hash()
+ checksum_size = None # filled in by create_hex_hash()
checksum_chars = uh.HEX_CHARS
+ #=========================================================
+ # methods
+ #=========================================================
@classmethod
- def identify(cls, hash):
- if not hash:
- return False
- if isinstance(hash, bytes):
- try:
- hash = hash.decode("ascii")
- except UnicodeDecodeError:
- return False
- cc = cls.checksum_chars
- return len(hash) == cls.checksum_size and all(c in cc for c in hash)
+ def _norm_hash(cls, hash):
+ return hash.lower()
- @classmethod
- def genhash(cls, secret, hash):
- if hash is not None and not cls.identify(hash):
- raise ValueError("not a %s hash" % (cls.name,))
- if secret is None:
- raise TypeError("no secret provided")
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return cls._hash_func(secret).hexdigest()
+ def _calc_checksum(self, secret):
+ secret = to_bytes(secret, "utf-8", errname="secret")
+ return str_to_uascii(self._hash_func(secret).hexdigest())
- @classmethod
- def _norm_hash(cls, hash):
- return to_native_str(hash, "ascii", errname="hash").lower()
+ #=========================================================
+ # eoc
+ #=========================================================
def create_hex_hash(hash, digest_name):
#NOTE: could set digest_name=hash.name for cpython, but not for some other platforms.
diff --git a/passlib/handlers/django.py b/passlib/handlers/django.py
index 901a0a0..b91c265 100644
--- a/passlib/handlers/django.py
+++ b/passlib/handlers/django.py
@@ -35,7 +35,7 @@ def _import_des_crypt():
#=========================================================
#salted hashes
#=========================================================
-class DjangoSaltedHash(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler):
+class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler):
"""base class providing common code for django hashes"""
#must be specified by subclass - along w/ calc_checksum
setting_kwds = ("salt", "salt_size")
@@ -48,10 +48,6 @@ class DjangoSaltedHash(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler):
salt_chars = checksum_chars = uh.LOWER_HEX_CHARS
@classmethod
- def identify(cls, hash):
- return uh.identify_prefix(hash, cls.ident)
-
- @classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
@@ -144,7 +140,7 @@ class django_des_crypt(DjangoSaltedHash):
"""
name = "django_des_crypt"
- ident = "crypt$"
+ ident = u("crypt$")
checksum_chars = salt_chars = uh.HASH64_CHARS
checksum_size = 13
min_salt_size = 2
@@ -162,9 +158,14 @@ class django_des_crypt(DjangoSaltedHash):
# else hash can *never* validate
salt = self.salt
chk = self.checksum
- if salt and chk and salt[:2] != chk[:2]:
- raise ValueError("invalid django_des_crypt hash: "
- "first two digits of salt and checksum must match")
+ if salt and chk:
+ if salt[:2] != chk[:2]:
+ raise ValueError("invalid django_des_crypt hash: "
+ "first two digits of salt and checksum must match")
+ # repeat stub checksum detection since salt isn't set
+ # when _norm_checksum() is called.
+ if chk == self._stub_checksum:
+ self.checksum = None
_base_stub_checksum = u('.') * 13
@@ -207,14 +208,15 @@ class django_disabled(uh.StaticHandler):
else:
return hash == u("!")
- @classmethod
- def genhash(cls, secret, config):
+ def _calc_checksum(self, secret):
if secret is None:
raise TypeError("no secret provided")
- return "!"
+ return u("!")
@classmethod
def verify(cls, secret, hash):
+ if secret is None:
+ raise TypeError("no secret provided")
if not cls.identify(hash):
raise ValueError("invalid django-disabled hash")
return False
diff --git a/passlib/handlers/fshp.py b/passlib/handlers/fshp.py
index 6141d4e..f24805d 100644
--- a/passlib/handlers/fshp.py
+++ b/passlib/handlers/fshp.py
@@ -23,7 +23,7 @@ __all__ = [
#=========================================================
#sha1-crypt
#=========================================================
-class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
+class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"""This class implements the FSHP password hash, and follows the :ref:`password-hash-api`.
It supports a variable-length salt, and a variable number of rounds.
@@ -58,6 +58,7 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u
name = "fshp"
setting_kwds = ("salt", "salt_size", "rounds", "variant")
checksum_chars = uh.PADDED_BASE64_CHARS
+ ident = u("{FSHP")
# checksum_size is property() that depends on variant
#--HasRawSalt--
@@ -129,11 +130,14 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u
#formatting
#=========================================================
- @classmethod
- def identify(cls, hash):
- return uh.identify_prefix(hash, u("{FSHP"))
-
- _fshp_re = re.compile(u(r"^\{FSHP(\d+)\|(\d+)\|(\d+)\}([a-zA-Z0-9+/]+={0,3})$"))
+ _hash_regex = re.compile(u(r"""
+ ^
+ \{FSHP
+ (\d+)\| # variant
+ (\d+)\| # salt size
+ (\d+)\} # rounds
+ ([a-zA-Z0-9+/]+={0,3}) # digest
+ $"""), re.X)
@classmethod
def from_string(cls, hash):
@@ -141,7 +145,7 @@ class fshp(uh.HasStubChecksum, uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, u
raise ValueError("no hash specified")
if isinstance(hash, bytes):
hash = hash.decode("ascii")
- m = cls._fshp_re.match(hash)
+ m = cls._hash_regex.match(hash)
if not m:
raise ValueError("not a valid FSHP hash")
variant, salt_size, rounds, data = m.group(1,2,3,4)
diff --git a/passlib/handlers/ldap_digests.py b/passlib/handlers/ldap_digests.py
index 38fe002..ce251eb 100644
--- a/passlib/handlers/ldap_digests.py
+++ b/passlib/handlers/ldap_digests.py
@@ -11,7 +11,9 @@ import re
from warnings import warn
#site
#libs
-from passlib.utils import to_native_str, unix_crypt_schemes
+from passlib.handlers.misc import plaintext
+from passlib.utils import to_native_str, unix_crypt_schemes, to_bytes, \
+ classproperty
from passlib.utils.compat import b, bytes, uascii_to_str, unicode, u
import passlib.utils.handlers as uh
#pkg
@@ -44,47 +46,37 @@ class _Base64DigestHelper(uh.StaticHandler):
ident = None #required - prefix identifier
_hash_func = None #required - hash function
- _pat = None #required - regexp to recognize hash
+ _hash_regex = None #required - regexp to recognize hash
checksum_chars = uh.PADDED_BASE64_CHARS
- @classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
+ @classproperty
+ def _hash_prefix(cls):
+ "tell StaticHandler to strip ident from checksum"
+ return cls.ident
- @classmethod
- def genhash(cls, secret, hash):
- if secret is None:
- raise TypeError("no secret provided")
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- if hash is not None and not cls.identify(hash):
- raise ValueError("not a %s hash" % (cls.name,))
- chk = cls._hash_func(secret).digest()
- hash = cls.ident + b64encode(chk).decode("ascii")
- return uascii_to_str(hash)
+ def _calc_checksum(self, secret):
+ secret = to_bytes(secret, "utf-8", errname="secret")
+ chk = self._hash_func(secret).digest()
+ return b64encode(chk).decode("ascii")
-class _SaltedBase64DigestHelper(uh.HasStubChecksum, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
+class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"helper for ldap_salted_md5 / ldap_salted_sha1"
setting_kwds = ("salt",)
checksum_chars = uh.PADDED_BASE64_CHARS
ident = None #required - prefix identifier
_hash_func = None #required - hash function
- _pat = None #required - regexp to recognize hash
+ _hash_regex = None #required - regexp to recognize hash
_stub_checksum = None #required - default checksum to plug in
min_salt_size = max_salt_size = 4
@classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
-
- @classmethod
def from_string(cls, hash):
if not hash:
raise ValueError("no hash specified")
if isinstance(hash, bytes):
hash = hash.decode('ascii')
- m = cls._pat.match(hash)
+ m = cls._hash_regex.match(hash)
if not m:
raise ValueError("not a %s hash" % (cls.name,))
data = b64decode(m.group("tmp").encode("ascii"))
@@ -116,7 +108,7 @@ class ldap_md5(_Base64DigestHelper):
ident = u("{MD5}")
_hash_func = md5
- _pat = re.compile(u(r"^\{MD5\}(?P<chk>[+/a-zA-Z0-9]{22}==)$"))
+ _hash_regex = re.compile(u(r"^\{MD5\}(?P<chk>[+/a-zA-Z0-9]{22}==)$"))
class ldap_sha1(_Base64DigestHelper):
"""This class stores passwords using LDAP's plain SHA1 format, and follows the :ref:`password-hash-api`.
@@ -128,7 +120,7 @@ class ldap_sha1(_Base64DigestHelper):
ident = u("{SHA}")
_hash_func = sha1
- _pat = re.compile(u(r"^\{SHA\}(?P<chk>[+/a-zA-Z0-9]{27}=)$"))
+ _hash_regex = re.compile(u(r"^\{SHA\}(?P<chk>[+/a-zA-Z0-9]{27}=)$"))
class ldap_salted_md5(_SaltedBase64DigestHelper):
"""This class stores passwords using LDAP's salted MD5 format, and follows the :ref:`password-hash-api`.
@@ -145,7 +137,7 @@ class ldap_salted_md5(_SaltedBase64DigestHelper):
name = "ldap_salted_md5"
ident = u("{SMD5}")
_hash_func = md5
- _pat = 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}=)$"))
_stub_checksum = b('\x00') * 16
class ldap_salted_sha1(_SaltedBase64DigestHelper):
@@ -163,10 +155,10 @@ class ldap_salted_sha1(_SaltedBase64DigestHelper):
name = "ldap_salted_sha1"
ident = u("{SSHA}")
_hash_func = sha1
- _pat = 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})$"))
_stub_checksum = b('\x00') * 20
-class ldap_plaintext(uh.StaticHandler):
+class ldap_plaintext(plaintext):
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
This class acts much like the generic :class:`!passlib.hash.plaintext` handler,
@@ -175,8 +167,10 @@ class ldap_plaintext(uh.StaticHandler):
Unicode passwords will be encoded using utf-8.
"""
- name = "ldap_plaintext"
+ # NOTE: this subclasses plaintext, since all it does differently
+ # is override identify()
+ name = "ldap_plaintext"
_2307_pat = re.compile(u(r"^\{\w+\}.*$"))
@classmethod
@@ -185,22 +179,12 @@ class ldap_plaintext(uh.StaticHandler):
return False
if isinstance(hash, bytes):
try:
- hash = hash.decode("utf-8")
+ hash = hash.decode(cls._hash_encoding)
except UnicodeDecodeError:
return False
- #NOTE: identifies all strings EXCEPT those which match...
+ # NOTE: identifies all strings EXCEPT those with {XXX} prefix
return cls._2307_pat.match(hash) is None
- @classmethod
- def genhash(cls, secret, hash):
- if hash is not None and not cls.identify(hash):
- raise ValueError("not a valid ldap_plaintext hash")
- return to_native_str(secret, "utf-8", errname="secret")
-
- @classmethod
- def _norm_hash(cls, hash):
- return to_native_str(hash, "utf-8", errname="hash")
-
#=========================================================
#{CRYPT} wrappers
#=========================================================
diff --git a/passlib/handlers/misc.py b/passlib/handlers/misc.py
index 06d9400..4244e14 100644
--- a/passlib/handlers/misc.py
+++ b/passlib/handlers/misc.py
@@ -8,8 +8,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import to_native_str
-from passlib.utils.compat import bytes, unicode
+from passlib.utils import to_native_str, consteq
+from passlib.utils.compat import bytes, unicode, u
import passlib.utils.handlers as uh
#pkg
#local
@@ -37,55 +37,82 @@ class unix_fallback(uh.StaticHandler):
"""
name = "unix_fallback"
context_kwds = ("enable_wildcard",)
- _stub_config = "!"
@classmethod
def identify(cls, hash):
return hash is not None
- @classmethod
- def genhash(cls, secret, hash, enable_wildcard=False):
+ def __init__(self, enable_wildcard=False, **kwds):
+ super(unix_fallback, self).__init__(**kwds)
+ self.enable_wildcard = enable_wildcard
+
+ def _calc_checksum(self, secret):
if secret is None:
raise TypeError("secret must be string")
- if hash is None:
- raise ValueError("no hash provided")
- # NOTE: hash will generally be "!"
- return to_native_str(hash, "ascii", errname="hash")
+ if self.checksum:
+ # NOTE: hash will generally be "!", but we want to preserve
+ # it in case it's something else, like "*".
+ return self.checksum
+ else:
+ return u("!")
@classmethod
def verify(cls, secret, hash, enable_wildcard=False):
- if hash is None:
+ if secret is None:
+ raise TypeError("secret must be string")
+ elif hash is None:
raise ValueError("no hash provided")
elif hash:
return False
else:
return enable_wildcard
-class plaintext(uh.StaticHandler):
+class plaintext(object):
"""This class stores passwords in plaintext, and follows the :ref:`password-hash-api`.
Unicode passwords will be encoded using utf-8.
Under Python 3, existing 'hashes' must decode as utf-8.
"""
+ # NOTE: this tries to avoid decoding bytes under py2,
+ # for applications that are using latin-1 or some other encoding.
+ # they'll just have to stop using plaintext under py3 :)
+ # (or re-encode as utf-8)
+
+ # NOTE: this is subclassed by ldap_plaintext
+
name = "plaintext"
+ setting_kwds = ()
+ context_kwds = ()
+ _hash_encoding = "utf-8"
@classmethod
def identify(cls, hash):
+ # by default, identify ALL strings
return hash is not None
- # NOTE: this tries to avoid decoding bytes under py2,
- # for applications that are using latin-1 or some other encoding.
- # they'll just have to stop using plaintext under py3 :)
- # (or re-encode as utf-8)
+ @classmethod
+ def encrypt(cls, secret):
+ return to_native_str(secret, cls._hash_encoding, "secret")
@classmethod
- def genhash(cls, secret, hash):
- return to_native_str(secret, "utf-8", errname="secret")
+ def verify(cls, secret, hash):
+ if hash is None:
+ raise TypeError("no hash specified")
+ elif not cls.identify(hash):
+ raise ValueError("not a %s hash" % (cls.name,))
+ hash = to_native_str(hash, cls._hash_encoding, "hash")
+ return consteq(cls.encrypt(secret), hash)
@classmethod
- def _norm_hash(cls, hash):
- return to_native_str(hash, "utf-8", errname="hash")
+ def genconfig(cls):
+ return None
+
+ @classmethod
+ def genhash(cls, secret, hash):
+ if hash is not None and not cls.identify(hash):
+ raise ValueError("not a %s hash" % (cls.name,))
+ return cls.encrypt(secret)
#=========================================================
#eof
diff --git a/passlib/handlers/mysql.py b/passlib/handlers/mysql.py
index cab378b..cea160e 100644
--- a/passlib/handlers/mysql.py
+++ b/passlib/handlers/mysql.py
@@ -30,8 +30,9 @@ from warnings import warn
#site
#libs
#pkg
-from passlib.utils import to_native_str
-from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, belem_ord
+from passlib.utils import to_native_str, to_bytes
+from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, \
+ belem_ord, str_to_uascii
import passlib.utils.handlers as uh
#local
__all__ = [
@@ -50,51 +51,41 @@ class mysql323(uh.StaticHandler):
The :meth:`encrypt()` and :meth:`genconfig` methods accept no optional keywords.
"""
#=========================================================
- #class attrs
+ # class attrs
#=========================================================
name = "mysql323"
+ checksum_size = 16
checksum_chars = uh.HEX_CHARS
- _pat = re.compile(u(r"^[0-9a-f]{16}$"), re.I)
-
#=========================================================
- #methods
+ # methods
#=========================================================
-
- @classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
-
@classmethod
- def genhash(cls, secret, config):
- if config is not None and not cls.identify(config):
- raise ValueError("not a mysql-3.2.3 hash")
+ def _norm_hash(cls, hash):
+ return hash.lower()
- #FIXME: no idea if mysql has a policy about handling unicode passwords
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
+ def _calc_checksum(self, secret):
+ # FIXME: no idea if mysql has a policy about handling unicode passwords
+ secret = to_bytes(secret, "utf-8", errname="secret")
MASK_32 = 0xffffffff
MASK_31 = 0x7fffffff
+ WHITE = b(' \t')
nr1 = 0x50305735
nr2 = 0x12345671
add = 7
for c in secret:
- if c in b(' \t'):
+ if c in WHITE:
continue
tmp = belem_ord(c)
nr1 ^= ((((nr1 & 63)+add)*tmp) + (nr1 << 8)) & MASK_32
nr2 = (nr2+((nr2 << 8) ^ nr1)) & MASK_32
add = (add+tmp) & MASK_32
- return "%08x%08x" % (nr1 & MASK_31, nr2 & MASK_31)
-
- @classmethod
- def _norm_hash(cls, hash):
- return to_native_str(hash, "ascii", errname="hash").lower()
+ return u("%08x%08x") % (nr1 & MASK_31, nr2 & MASK_31)
#=========================================================
- #eoc
+ # eoc
#=========================================================
#=========================================================
@@ -108,34 +99,27 @@ class mysql41(uh.StaticHandler):
The :meth:`encrypt()` and :meth:`genconfig` methods accept no optional keywords.
"""
#=========================================================
- #class attrs
+ # class attrs
#=========================================================
name = "mysql41"
- _pat = re.compile(r"^\*[0-9A-F]{40}$", re.I)
+ _hash_prefix = u("*")
+ checksum_chars = uh.HEX_CHARS
+ checksum_size = 40
#=========================================================
- #methods
+ # methods
#=========================================================
-
@classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
+ def _norm_hash(cls, hash):
+ return hash.upper()
- @classmethod
- def genhash(cls, secret, config):
- if config is not None and not cls.identify(config):
- raise ValueError("not a mysql-4.1 hash")
+ def _calc_checksum(self, secret):
# FIXME: no idea if mysql has a policy about handling unicode passwords
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- return '*' + sha1(sha1(secret).digest()).hexdigest().upper()
-
- @classmethod
- def _norm_hash(cls, hash):
- return to_native_str(hash, "ascii", errname="hash").upper()
+ secret = to_bytes(secret, "utf-8", errname="secret")
+ return str_to_uascii(sha1(sha1(secret).digest()).hexdigest()).upper()
#=========================================================
- #eoc
+ # eoc
#=========================================================
#=========================================================
diff --git a/passlib/handlers/nthash.py b/passlib/handlers/nthash.py
index 6ad56c2..4b29b0e 100644
--- a/passlib/handlers/nthash.py
+++ b/passlib/handlers/nthash.py
@@ -21,7 +21,7 @@ __all__ = [
#=========================================================
#handler
#=========================================================
-class nthash(uh.HasStubChecksum, uh.HasManyIdents, uh.GenericHandler):
+class nthash(uh.HasManyIdents, uh.GenericHandler):
"""This class implements the NT Password hash in a manner compatible with the :ref:`modular-crypt-format`, and follows the :ref:`password-hash-api`.
It has no salt and a single fixed round.
@@ -56,16 +56,7 @@ class nthash(uh.HasStubChecksum, uh.HasManyIdents, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise ValueError("no hash specified")
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
- for ident in cls.ident_values:
- if hash.startswith(ident):
- break
- else:
- raise ValueError("invalid nthash")
- chk = hash[len(ident):]
+ ident, chk = cls._parse_ident(hash)
return cls(ident=ident, checksum=chk)
def to_string(self):
diff --git a/passlib/handlers/oracle.py b/passlib/handlers/oracle.py
index 61ad27a..1322ea9 100644
--- a/passlib/handlers/oracle.py
+++ b/passlib/handlers/oracle.py
@@ -51,7 +51,7 @@ def des_cbc_encrypt(key, value, iv=b('\x00') * 8, pad=b('\x00')):
#: magic string used as initial des key by oracle10
ORACLE10_MAGIC = b("\x01\x23\x45\x67\x89\xAB\xCD\xEF")
-class oracle10(uh.StaticHandler):
+class oracle10(uh.HasUserContext, uh.StaticHandler):
"""This class implements the password hash used by Oracle up to version 10g, and follows the :ref:`password-hash-api`.
It has no salt and a single fixed round.
@@ -64,62 +64,37 @@ class oracle10(uh.StaticHandler):
:param user: string containing name of oracle user account this password is associated with.
"""
#=========================================================
- #algorithm information
+ # algorithm information
#=========================================================
name = "oracle10"
- setting_kwds = ()
- context_kwds = ("user",)
+ checksum_chars = uh.HEX_CHARS
+ checksum_size = 16
#=========================================================
- #formatting
+ # methods
#=========================================================
- _pat = re.compile(u(r"^[0-9a-fA-F]{16}$"))
-
@classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
-
- #=========================================================
- #primary interface
- #=========================================================
- @classmethod
- def genhash(cls, secret, config, user):
- if config is not None and not cls.identify(config):
- raise ValueError("not an oracle-10g hash")
- if secret is None:
- raise TypeError("secret must be specified")
- if not user:
- raise ValueError("user keyword must be specified for this algorithm")
+ def _norm_hash(cls, hash):
+ return hash.upper()
+ def _calc_checksum(self, secret):
#FIXME: not sure how oracle handles unicode.
# online docs about 10g hash indicate it puts ascii chars
- # in a 2-byte encoding w/ the high bytenull.
- # they don't say how it handles other chars,
- # or what encoding.
+ # in a 2-byte encoding w/ the high byte set to null.
+ # they don't say how it handles other chars, or what encoding.
#
- # so for now, encoding secret & user to utf-16-be,
- # since that fits,
+ # so for now, encoding secret & user to utf-16-be, since that fits,
# and if secret/user is bytes, we assume utf-8, and decode first.
#
# this whole mess really needs someone w/ an oracle system,
# and some answers :)
- def encode(value, errname):
- "encode according to guess at how oracle encodes strings (see note above)"
- #we can't trust what original encoding was.
- #user should have passed us unicode in the first place.
- #but try decoding as utf-8 just to work for most common case.
- value = to_unicode(value, "utf-8", errname=errname)
- return value.upper().encode("utf-16-be")
-
- input = encode(user, 'user') + encode(secret, 'secret')
+ secret = to_unicode(secret, "utf-8", errname="secret")
+ user = to_unicode(self.user, "utf-8", errname="user")
+ input = (user+secret).upper().encode("utf-16-be")
hash = des_cbc_encrypt(ORACLE10_MAGIC, input)
hash = des_cbc_encrypt(hash, input)
- return bascii_to_str(hexlify(hash)).upper()
-
- @classmethod
- def _norm_hash(cls, hash):
- return to_native_str(hash, "ascii", errname="hash").upper()
+ return hexlify(hash).decode("ascii").upper()
#=========================================================
#eoc
@@ -128,7 +103,7 @@ class oracle10(uh.StaticHandler):
#=========================================================
#oracle11
#=========================================================
-class oracle11(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler):
+class oracle11(uh.HasSalt, uh.GenericHandler):
"""This class implements the Oracle11g password hash, and follows the :ref:`password-hash-api`.
It supports a fixed-length salt.
@@ -159,11 +134,7 @@ class oracle11(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler):
#=========================================================
#methods
#=========================================================
- _pat = re.compile(u("^S:(?P<chk>[0-9a-f]{40})(?P<salt>[0-9a-f]{20})$"), re.I)
-
- @classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
+ _hash_regex = re.compile(u("^S:(?P<chk>[0-9a-f]{40})(?P<salt>[0-9a-f]{20})$"), re.I)
@classmethod
def from_string(cls, hash):
@@ -171,7 +142,7 @@ class oracle11(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler):
raise ValueError("no hash provided")
if isinstance(hash, bytes):
hash = hash.decode("ascii")
- m = cls._pat.match(hash)
+ m = cls._hash_regex.match(hash)
if not m:
raise ValueError("invalid oracle-11g hash")
salt, chk = m.group("salt", "chk")
diff --git a/passlib/handlers/pbkdf2.py b/passlib/handlers/pbkdf2.py
index 37dab77..477e09d 100644
--- a/passlib/handlers/pbkdf2.py
+++ b/passlib/handlers/pbkdf2.py
@@ -184,7 +184,7 @@ class cta_pbkdf2_sha1(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.Generic
#--HasROunds--
default_rounds = 10000
- min_rounds = 0
+ min_rounds = 1
max_rounds = 2**32-1
rounds_cost = "linear"
@@ -281,7 +281,7 @@ class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
#--HasROunds--
default_rounds = 10000
- min_rounds = 0
+ min_rounds = 1
max_rounds = 2**32-1
rounds_cost = "linear"
@@ -336,7 +336,7 @@ class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
#=========================================================
#crowd
#=========================================================
-class atlassian_pbkdf2_sha1(uh.HasStubChecksum, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
+class atlassian_pbkdf2_sha1(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
"""This class implements the PBKDF2 hash used by Atlassian.
It supports a fixed-length salt, and a fixed number of rounds.
diff --git a/passlib/handlers/phpass.py b/passlib/handlers/phpass.py
index 3273d61..c093255 100644
--- a/passlib/handlers/phpass.py
+++ b/passlib/handlers/phpass.py
@@ -86,16 +86,7 @@ class phpass(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise ValueError("no hash specified")
- if isinstance(hash, bytes):
- hash = hash.decode('ascii')
- for ident in cls.ident_values:
- if hash.startswith(ident):
- break
- else:
- raise ValueError("invalid phpass portable hash")
- data = hash[len(ident):]
+ ident, data = cls._parse_ident(hash)
rounds, salt, chk = data[0], data[1:9], data[9:]
return cls(
ident=ident,
diff --git a/passlib/handlers/postgres.py b/passlib/handlers/postgres.py
index c4b5f9a..63e7ddd 100644
--- a/passlib/handlers/postgres.py
+++ b/passlib/handlers/postgres.py
@@ -10,8 +10,8 @@ from warnings import warn
#site
#libs
#pkg
-from passlib.utils import to_unicode
-from passlib.utils.compat import b, bytes, bascii_to_str, unicode, u
+from passlib.utils import to_bytes
+from passlib.utils.compat import b, bytes, str_to_uascii, unicode, u
import passlib.utils.handlers as uh
#local
__all__ = [
@@ -21,7 +21,7 @@ __all__ = [
#=========================================================
#handler
#=========================================================
-class postgres_md5(uh.StaticHandler):
+class postgres_md5(uh.HasUserContext, uh.StaticHandler):
"""This class implements the Postgres MD5 Password hash, and follows the :ref:`password-hash-api`.
It has no salt and a single fixed round.
@@ -34,35 +34,20 @@ class postgres_md5(uh.StaticHandler):
:param user: string containing name of postgres user account this password is associated with.
"""
#=========================================================
- #algorithm information
+ # algorithm information
#=========================================================
name = "postgres_md5"
- setting_kwds = ()
- context_kwds = ("user",)
+ _hash_prefix = u("md5")
+ checksum_chars = uh.HEX_CHARS
+ checksum_size = 32
#=========================================================
- #formatting
+ # primary interface
#=========================================================
- _pat = re.compile(u(r"^md5[0-9a-f]{32}$"))
-
- @classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, cls._pat)
-
- #=========================================================
- #primary interface
- #=========================================================
- @classmethod
- def genhash(cls, secret, config, user):
- if config is not None and not cls.identify(config):
- raise ValueError("not a postgres-md5 hash")
- if not user:
- raise ValueError("user keyword must be specified for this algorithm")
- if isinstance(secret, unicode):
- secret = secret.encode("utf-8")
- if isinstance(user, unicode):
- user = user.encode("utf-8")
- return "md5" + md5(secret + user).hexdigest()
+ def _calc_checksum(self, secret):
+ secret = to_bytes(secret, "utf-8", errname="secret")
+ user = to_bytes(self.user, "utf-8", errname="user")
+ return str_to_uascii(md5(secret + user).hexdigest())
#=========================================================
#eoc
diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py
index 64274d9..bffa5c1 100644
--- a/passlib/handlers/sha2_crypt.py
+++ b/passlib/handlers/sha2_crypt.py
@@ -115,7 +115,7 @@ def _raw_sha_crypt(secret, salt, rounds, hash):
# +
# if i%2>0 then C else DP
#
- # The algorithm can be see as a series of paired even/odd rounds,
+ # The algorithm can be seen as a series of paired even/odd rounds,
# with each pair performing 'C = md5(odd_data + md5(C + even_data))',
# where even_data & odd_data cycle through a fixed series of
# combinations of DP & DS, repeating every 42 rounds (since lcm(2,3,7)==42)
@@ -123,7 +123,7 @@ def _raw_sha_crypt(secret, salt, rounds, hash):
# This code takes advantage of these facts: it precalculates all possible
# combinations, and then orders them into 21 pairs of even,odd values.
# this allows the brunt of C stage to be performed in 42-round blocks,
- # with minimal overhead.
+ # with minimal branching/concatenation overhead.
# build array containing 42-round pattern as pairs of even & odd data.
dp_dp = dp*2
@@ -281,7 +281,7 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
#=========================================================
#: regexp used to parse hashes
- _pat = re.compile(u(r"""
+ _hash_regex = re.compile(u(r"""
^
\$5
(\$rounds=(?P<rounds>\d+))?
@@ -302,7 +302,7 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
raise ValueError("no hash specified")
if isinstance(hash, bytes):
hash = hash.decode("ascii")
- m = cls._pat.match(hash)
+ m = cls._hash_regex.match(hash)
if not m:
raise ValueError("invalid sha256-crypt hash")
rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk")
@@ -435,7 +435,7 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
#=========================================================
#: regexp used to parse hashes
- _pat = re.compile(u(r"""
+ _hash_regex = re.compile(u(r"""
^
\$6
(\$rounds=(?P<rounds>\d+))?
@@ -458,7 +458,7 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl
raise ValueError("no hash specified")
if isinstance(hash, bytes):
hash = hash.decode("ascii")
- m = cls._pat.match(hash)
+ m = cls._hash_regex.match(hash)
if not m:
raise ValueError("invalid sha512-crypt hash")
rounds, salt1, salt2, chk = m.group("rounds", "salt1", "salt2", "chk")
diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py
index c441901..7d689e7 100644
--- a/passlib/handlers/sun_md5_crypt.py
+++ b/passlib/handlers/sun_md5_crypt.py
@@ -216,6 +216,8 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
#XXX: ^ not sure what it does if past this bound... does 32 int roll over?
rounds_cost = "linear"
+ ident_values = (u("$md5$"), u("$md5,"))
+
#=========================================================
#instance attrs
#=========================================================
@@ -233,7 +235,14 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
#=========================================================
@classmethod
def identify(cls, hash):
- return uh.identify_prefix(hash, (u("$md5$"), u("$md5,")))
+ if not hash:
+ return False
+ if isinstance(hash, bytes):
+ try:
+ hash = hash.decode("ascii")
+ except UnicodeDecodeError:
+ return False
+ return hash.startswith(cls.ident_values)
@classmethod
def from_string(cls, hash):
diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py
index 151af5a..c3bf622 100644
--- a/passlib/tests/test_context.py
+++ b/passlib/tests/test_context.py
@@ -937,10 +937,9 @@ class CryptContextTest(TestCase):
def identify(cls, hash):
return True
- @classmethod
- def genhash(cls, secret, hash):
+ def _calc_checksum(self, secret):
time.sleep(cls.delay)
- return secret + 'x'
+ return to_unicode(secret + 'x')
# silence deprecation warnings for min verify time
with catch_warnings(record=True) as wlog:
diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py
index 278d252..8ae5a96 100644
--- a/passlib/tests/test_utils_handlers.py
+++ b/passlib/tests/test_utils_handlers.py
@@ -39,61 +39,58 @@ def _makelang(alphabet, size):
return set(helper(size))
#=========================================================
-#test support classes - StaticHandler, GenericHandler, etc
+#test GenericHandler & associates mixin classes
#=========================================================
class SkeletonTest(TestCase):
"test hash support classes"
#=========================================================
- #StaticHandler
+ # StaticHandler
#=========================================================
def test_00_static_handler(self):
- "test StaticHandler helper class"
+ "test StaticHandler class"
class d1(uh.StaticHandler):
name = "d1"
context_kwds = ("flag",)
+ _hash_prefix = u("_")
+ checksum_chars = u("ab")
+ checksum_size = 1
- @classmethod
- def genhash(cls, secret, hash, flag=False):
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
- if hash not in (u('a'), u('b'), None):
- raise ValueError("unknown hash %r" % (hash,))
- return 'b' if flag else 'a'
+ def __init__(self, flag=False, **kwds):
+ super(d1, self).__init__(**kwds)
+ self.flag = flag
+
+ def _calc_checksum(self, secret):
+ return u('b') if self.flag else u('a')
# check default identify method
- self.assertTrue(d1.identify(u('a')))
- self.assertTrue(d1.identify(b('a')))
- self.assertTrue(d1.identify(u('b')))
+ self.assertTrue(d1.identify(u('_a')))
+ self.assertTrue(d1.identify(b('_a')))
+ self.assertTrue(d1.identify(u('_b')))
+
+ self.assertFalse(d1.identify(u('_c')))
+ self.assertFalse(d1.identify(b('_c')))
+ self.assertFalse(d1.identify(u('a')))
+ self.assertFalse(d1.identify(u('b')))
self.assertFalse(d1.identify(u('c')))
- self.assertFalse(d1.identify(b('c')))
- self.assertFalse(d1.identify(u('')))
self.assertFalse(d1.identify(None))
# check default genconfig method
self.assertIs(d1.genconfig(), None)
- d1._stub_config = u('b')
- self.assertEqual(d1.genconfig(), 'b')
-
- # check config string is rejected
- self.assertRaises(ValueError, d1.verify, 's', b('b'))
- self.assertRaises(ValueError, d1.verify, 's', u('b'))
- del d1._stub_config
# check default verify method
- self.assertTrue(d1.verify('s', b('a')))
- self.assertTrue(d1.verify('s',u('a')))
- self.assertFalse(d1.verify('s', b('b')))
- self.assertFalse(d1.verify('s',u('b')))
- self.assertTrue(d1.verify('s', b('b'), flag=True))
- self.assertRaises(ValueError, d1.verify, 's', b('c'))
- self.assertRaises(ValueError, d1.verify, 's', u('c'))
+ self.assertTrue(d1.verify('s', b('_a')))
+ self.assertTrue(d1.verify('s',u('_a')))
+ self.assertFalse(d1.verify('s', b('_b')))
+ self.assertFalse(d1.verify('s',u('_b')))
+ self.assertTrue(d1.verify('s', b('_b'), flag=True))
+ self.assertRaises(ValueError, d1.verify, 's', b('_c'))
+ self.assertRaises(ValueError, d1.verify, 's', u('_c'))
# check default encrypt method
- self.assertEqual(d1.encrypt('s'), 'a')
- self.assertEqual(d1.encrypt('s'), 'a')
- self.assertEqual(d1.encrypt('s', flag=True), 'b')
+ self.assertEqual(d1.encrypt('s'), '_a')
+ self.assertEqual(d1.encrypt('s', flag=True), '_b')
#=========================================================
#GenericHandler & mixins
@@ -104,8 +101,10 @@ class SkeletonTest(TestCase):
@classmethod
def from_string(cls, hash):
- if hash == 'a':
- return cls(checksum='a')
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
+ if hash == u('a'):
+ return cls(checksum=hash)
else:
raise ValueError
@@ -115,12 +114,21 @@ class SkeletonTest(TestCase):
self.assertTrue(d1.identify('a'))
self.assertFalse(d1.identify('b'))
+ # check regexp
+ d1._hash_regex = re.compile(u('@.'))
+ self.assertFalse(d1.identify(None))
+ self.assertFalse(d1.identify(''))
+ self.assertTrue(d1.identify('@a'))
+ self.assertFalse(d1.identify('a'))
+ del d1._hash_regex
+
# check ident-based
d1.ident = u('!')
self.assertFalse(d1.identify(None))
self.assertFalse(d1.identify(''))
self.assertTrue(d1.identify('!a'))
self.assertFalse(d1.identify('a'))
+ del d1.ident
def test_11_norm_checksum(self):
"test GenericHandler checksum handling"
@@ -128,22 +136,32 @@ class SkeletonTest(TestCase):
class d1(uh.GenericHandler):
name = 'd1'
checksum_size = 4
- checksum_chars = 'x'
+ checksum_chars = u('xz')
+ _stub_checksum = u('z')*4
def norm_checksum(*a, **k):
return d1(*a, **k).checksum
# too small
- self.assertRaises(ValueError, norm_checksum, 'xxx')
+ self.assertRaises(ValueError, norm_checksum, u('xxx'))
# right size
- self.assertEqual(norm_checksum('xxxx'), 'xxxx')
+ self.assertEqual(norm_checksum(u('xxxx')), u('xxxx'))
+ self.assertEqual(norm_checksum(u('xzxz')), u('xzxz'))
# too large
- self.assertRaises(ValueError, norm_checksum, 'xxxxx')
+ self.assertRaises(ValueError, norm_checksum, u('xxxxx'))
# wrong chars
- self.assertRaises(ValueError, norm_checksum, 'xxyx')
+ self.assertRaises(ValueError, norm_checksum, u('xxyx'))
+
+ # wrong type
+ self.assertRaises(TypeError, norm_checksum, b('xxyx'))
+
+ # test _stub_checksum behavior
+ self.assertIs(norm_checksum(u('zzzz')), None)
+
+ # TODO: test HasRawChecksum mixin
def test_20_norm_salt(self):
"test GenericHandler + HasSalt mixin"
@@ -220,6 +238,8 @@ class SkeletonTest(TestCase):
self.assertEqual(len(gen_salt(5)), 5)
self.consumeWarningList(wlog)
+ # TODO: test HasRawSalt mixin
+
def test_30_norm_rounds(self):
"test GenericHandler + HasRounds mixin"
# setup helpers
@@ -504,22 +524,16 @@ class PrefixWrapperTest(TestCase):
class UnsaltedHash(uh.StaticHandler):
"test algorithm which lacks a salt"
name = "unsalted_test_hash"
- _stub_config = "0" * 40
-
- @classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, re.compile(u("^[0-9a-f]{40}$")))
+ checksum_chars = uh.LOWER_HEX_CHARS
+ checksum_size = 40
- @classmethod
- def genhash(cls, secret, hash):
- if not cls.identify(hash):
- raise ValueError("not a unsalted-example hash")
+ def _calc_checksum(self, secret):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
data = b("boblious") + secret
- return hashlib.sha1(data).hexdigest()
+ return str_to_uascii(hashlib.sha1(data).hexdigest())
-class SaltedHash(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler):
+class SaltedHash(uh.HasSalt, uh.GenericHandler):
"test algorithm with a salt"
name = "salted_test_hash"
setting_kwds = ("salt",)
@@ -529,9 +543,7 @@ class SaltedHash(uh.HasStubChecksum, uh.HasSalt, uh.GenericHandler):
checksum_size = 40
salt_chars = checksum_chars = uh.LOWER_HEX_CHARS
- @classmethod
- def identify(cls, hash):
- return uh.identify_regexp(hash, re.compile(u("^@salt[0-9a-f]{42,44}$")))
+ _hash_regex = re.compile(u("^@salt[0-9a-f]{42,44}$"))
@classmethod
def from_string(cls, hash):
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index 139143b..911bba2 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -1,9 +1,9 @@
"""passlib.handler - code for implementing handlers, and global registry for handlers"""
#=========================================================
-#imports
+# imports
#=========================================================
from __future__ import with_statement
-#core
+# core
import inspect
import re
import hashlib
@@ -11,30 +11,36 @@ import logging; log = logging.getLogger(__name__)
import time
import os
from warnings import warn
-#site
-#libs
-from passlib.exc import MissingBackendError, PasslibHashWarning, \
- PasslibRuntimeWarning
+# site
+# pkg
+from passlib.exc import MissingBackendError, PasslibConfigWarning, \
+ PasslibHashWarning
from passlib.registry import get_crypt_handler
-from passlib.utils import is_crypt_handler
from passlib.utils import classproperty, consteq, getrandstr, getrandbytes,\
- BASE64_CHARS, HASH64_CHARS, rng, to_native_str
+ BASE64_CHARS, HASH64_CHARS, rng, to_native_str, \
+ is_crypt_handler, deprecated_function, to_unicode
from passlib.utils.compat import b, bjoin_ints, bytes, irange, u, \
- uascii_to_str, ujoin, unicode
-#pkg
-#local
+ uascii_to_str, ujoin, unicode, str_to_uascii
+# local
__all__ = [
- #framework for implementing handlers
- 'StaticHandler',
+ # helpers for implementing MCF handlers
+ 'parse_mc2',
+ 'parse_mc3',
+ 'render_mc2',
+ 'render_mc3',
+
+ # framework for implementing handlers
'GenericHandler',
- # checksum mixins
- 'HasRawChecksum',
- 'HasStubChecksum',
+ 'StaticHandler',
+ 'HasUserContext',
+ 'HasRawChecksum',
'HasManyIdents',
'HasSalt',
- 'HasRawSalt',
+ 'HasRawSalt',
'HasRounds',
'HasManyBackends',
+
+ # other helpers
'PrefixWrapper',
]
@@ -61,32 +67,6 @@ UC_HEX_CHARS = UPPER_HEX_CHARS
LC_HEX_CHARS = LOWER_HEX_CHARS
#=========================================================
-#identify helpers
-#=========================================================
-def identify_regexp(hash, pat):
- "identify() helper for matching regexp"
- if not hash:
- return False
- if isinstance(hash, bytes):
- try:
- hash = hash.decode("ascii")
- except UnicodeDecodeError:
- return False
- return pat.match(hash) is not None
-
-def identify_prefix(hash, prefix):
- "identify() helper for matching against prefixes"
- #NOTE: prefix may be a tuple of strings (since startswith supports that)
- if not hash:
- return False
- if isinstance(hash, bytes):
- try:
- hash = hash.decode("ascii")
- except UnicodeDecodeError:
- return False
- return hash.startswith(prefix)
-
-#=========================================================
#parsing helpers
#=========================================================
def parse_mc2(hash, prefix, name="<unnamed>", sep=u("$")):
@@ -149,119 +129,21 @@ def render_mc3(ident, rounds, salt, checksum, sep=u("$")):
hash = u("%s%s%s%s") % (ident, rounds, sep, salt)
return uascii_to_str(hash)
-#=====================================================
-#StaticHandler
-#=====================================================
-class StaticHandler(object):
- """helper class for implementing hashes which have no settings.
-
- This class is designed to help in writing hash handlers
- which have no settings whatsoever; that is to say: no salt, no rounds, etc.
- These hashes can typically be recognized by the fact that they
- will always hash a password to *exactly* the same hash string.
-
- Usage
- =====
-
- In order to use this class, just subclass it, and then do the following:
-
- * fill out the :attr:`name` attribute with the name of your hash.
- * provide an implementation of the :meth:`~PasswordHash.genhash` method.
- * provide an implementation of the :meth:`~PasswordHash.identify` method.
- (a default is provided, but it's inefficient).
-
- Based on the methods above, this class provides:
-
- * a :meth:`genconfig` method that returns ``None``.
- * a :meth:`encrypt` method that wraps :meth:`genhash`.
- * a :meth:`verify` method that wraps :meth:`genhash`.
-
- Implementation Details
- ======================
-
- The :meth:`genhash` method you implement must accept
- all valid hashes, *as well as* whatever value :meth:`genconfig` returns.
- This defaults to ``None``, but you may set the :attr:`_stub_config` attr
- to a specific hash string, and :meth:`genconfig` will return this instead.
-
- The default :meth:`verify` method uses simple equality to compare hash strings.
- If your hash has multiple encodings (e.g. is case-insensitive), the
- :meth:`_norm_hash` method should be overridden to normalize to a single
- representation.
-
- If your hash has options, such as multiple identifiers, salts,
- or variable rounds, this is not the right class to start with.
- You should use the :class:`GenericHandler` class, or implement the handler
- yourself.
- """
-
- #=====================================================
- #class attrs
- #=====================================================
- name = None #required - handler name
- setting_kwds = ()
- context_kwds = ()
-
- # reserved value to be returned by default genconfig()
- # may be ``None`` if no such value; otherwise should be native ascii str.
- _stub_config = None
-
- #=====================================================
- #methods
- #=====================================================
- @classmethod
- def identify(cls, hash):
- #NOTE: this relys on genhash() throwing error for invalid hashes.
- # this approach is bad because genhash may take a long time on valid hashes,
- # so subclasses *really* should override this.
- if hash is None:
- return False
- try:
- cls.genhash('fakesecret', hash)
- return True
- except ValueError:
- return False
-
- @classmethod
- def genconfig(cls):
- "default genconfig() implementation for unsalted hash algorithms"
- return cls._stub_config
-
- @classmethod
- def genhash(cls, secret, config, **context):
- raise NotImplementedError("%s subclass must implement genhash()" % (cls,))
-
- @classmethod
- def encrypt(cls, secret, *cargs, **context):
- "default encrypt() implementation for unsalted hash algorithms"
- # NOTE: subclasses generally won't need to override this
- config = cls.genconfig()
- return cls.genhash(secret, config, *cargs, **context)
-
- @classmethod
- def verify(cls, secret, hash, *cargs, **context):
- "default verify() implementation for unsalted hash algorithms"
- # NOTE: subclasses generally won't need to override this.
- if hash is None:
- raise ValueError("no hash specified")
- hash = cls._norm_hash(hash)
- if hash == cls._stub_config:
- raise ValueError("expected %s hash, got %s config string instead" %
- (cls.name, cls.name))
- result = cls.genhash(secret, hash, *cargs, **context)
- return consteq(result, hash)
-
- @classmethod
- def _norm_hash(cls, hash):
- """[helper for verify] normalize hash for comparsion purposes.
-
- should return a native :class:`str` instance or raise a TypeError.
- """
- return to_native_str(hash, "ascii", errname="hash")
-
- #=====================================================
- #eoc
- #=====================================================
+#==========================================================================
+# not proper exceptions, just predefined error message constructors
+# used by various handlers.
+#==========================================================================
+def ChecksumSizeError(handler, size, raw=False):
+ name = handler.name
+ unit = "bytes" if raw else "chars"
+ return ValueError("checksum wrong size (%s checksum must be "
+ "exactly %d %s" % (name, size, unit))
+
+def MissingDigestError(handler):
+ "raised when verify() method gets passed config string instead of hash"
+ name = handler.name
+ return ValueError("expected %s hash, got %s config string instead" %
+ (name, name))
#=====================================================
#GenericHandler
@@ -316,6 +198,15 @@ class GenericHandler(object):
This should be a unicode str.
+ .. attribute:: _hash_regex
+
+ [optional]
+ If this attribute is filled in, the default :meth:`identify` method
+ will use it to recognize instances of the hash. If :attr:`ident`
+ is specified, this will be ignored.
+
+ This should be a unique regex object.
+
.. attribute:: checksum_size
[optional]
@@ -330,11 +221,23 @@ class GenericHandler(object):
This should be a unicode str.
+ .. attribute:: _stub_checksum
+
+ [optional]
+ If specified, hashes with this checksum will have their checksum
+ normalized to ``None``, treating it like a config string.
+ This is mainly used by hash formats which don't have a concept
+ of a config string, so a unlikely-to-occur checksum (e.g. all zeros)
+ is used by some implementations.
+
+ This should be a string of the same datatype as :attr:`checksum`,
+ or ``None``.
+
Instance Attributes
===================
.. attribute:: checksum
- The checksum string as provided by the constructor (after passing it
+ The checksum string provided to the constructor (after passing it
through :meth:`_norm_checksum`).
Required Subclass Methods
@@ -347,8 +250,8 @@ class GenericHandler(object):
Default Methods
===============
- The following methods provide generally useful default behaviors,
- though they may be overridden if the hash subclass needs to:
+ The following methods have default implementations that should work for
+ most cases, though they may be overridden if the hash subclass needs to:
.. automethod:: _norm_checksum
@@ -362,19 +265,38 @@ class GenericHandler(object):
#=====================================================
#class attr
#=====================================================
+ # this must be provided by the actual class.
+ setting_kwds = None
+
+ # providing default since most classes don't use this at all.
context_kwds = ()
- ident = None #identifier prefix if known
+ # optional prefix that uniquely identifies hash
+ ident = None
+
+ # optional regexp for recognizing hashes,
+ # used by default identify() if .ident isn't specified.
+ _hash_regex = None
+
+ # if specified, _norm_checksum will require this length
+ checksum_size = None
+
+ # if specified, _norm_checksum() will validate this
+ checksum_chars = None
+
+ # if specified, hashes with this checksum will be treated
+ # as if no checksum was specified.
+ _stub_checksum = None
- checksum_size = None #if specified, _norm_checksum will require this length
- checksum_chars = None #if specified, _norm_checksum() will validate this
+ # private flag used by HasRawChecksum
+ _checksum_is_bytes = False
#=====================================================
#instance attrs
#=====================================================
checksum = None # stores checksum
-# relaxed = False # when norm_xxx() funcs should be strict about inputs
-# use_defaults = False # whether norm_xxx() funcs should fill in defaults.
+# use_defaults = False # whether _norm_xxx() funcs should fill in defaults.
+# relaxed = False # when _norm_xxx() funcs should be strict about inputs
#=====================================================
#init
@@ -390,30 +312,43 @@ class GenericHandler(object):
"""validates checksum keyword against class requirements,
returns normalized version of checksum.
"""
- # NOTE: this code assumes checksum should be a unicode string.
- # For classes where the checksum is raw bytes, the HasRawChecksum
- # mixin overrides this method with a more appropriate one.
+ # NOTE: by default this code assumes checksum should be unicode.
+ # For classes where the checksum is raw bytes, the HasRawChecksum sets
+ # the _checksum_is_bytes flag which alters various code paths below.
if checksum is None:
return None
- # normalize to unicode
- if isinstance(checksum, bytes):
- checksum = checksum.decode('ascii')
+ # normalize to bytes / unicode
+ raw = self._checksum_is_bytes
+ if raw:
+ # NOTE: no clear route to reasonbly convert unicode -> raw bytes,
+ # so relaxed does nothing here
+ if not isinstance(checksum, bytes):
+ raise TypeError("checksum must be byte string")
+
+ elif not isinstance(checksum, unicode):
+ if self.relaxed:
+ warn("checksum should be unicode, not bytes",
+ PasslibHashWarning)
+ checksum = checksum.decode("ascii")
+ else:
+ raise TypeError("checksum must be unicode string")
+
+ # handle stub
+ if checksum == self._stub_checksum:
+ return None
# check size
cc = self.checksum_size
if cc and len(checksum) != cc:
- raise ValueError("checksum wrong size (%s checksum must be "
- "exactly %d characters" % (self.name, cc))
+ raise ChecksumSizeError(self, cc, raw=raw)
# check charset
- cs = self.checksum_chars
- if cs:
- bad = set(checksum)
- bad.difference_update(cs)
- if bad:
- raise ValueError("invalid characters in %s checksum: %r" %
- (self.name, ujoin(sorted(bad))))
+ if not raw:
+ cs = self.checksum_chars
+ if cs and any(c not in cs for c in checksum):
+ raise ValueError("invalid characters in %s checksum" %
+ (self.name,))
return checksum
@@ -425,29 +360,43 @@ class GenericHandler(object):
# NOTE: subclasses may wish to use faster / simpler identify,
# and raise value errors only when an invalid (but identifiable)
# string is parsed
+
if not hash:
return False
+
+ # does class specify a known unique prefix to look for?
ident = cls.ident
- if ident:
- #class specified a known prefix to look for
+ if ident is not None:
assert isinstance(ident, unicode)
if isinstance(hash, bytes):
ident = ident.encode('ascii')
return hash.startswith(ident)
- else:
- # don't have known ident prefix; so as fallback, try to parse hash
- # to trying to parse hash and see if we succeed.
- # (inefficient, but works for most cases)
- try:
- cls.from_string(hash)
- return True
- except ValueError:
- return False
+
+ # does class provide a regexp to use?
+ pat = cls._hash_regex
+ if pat is not None:
+ if isinstance(hash, bytes):
+ try:
+ hash = hash.decode("ascii")
+ except UnicodeDecodeError:
+ return False
+ return pat.match(hash) is not None
+
+ # as fallback, try to parse hash, and see if we succeed.
+ # inefficient, but works for most cases.
+ try:
+ cls.from_string(hash)
+ return True
+ except ValueError:
+ return False
@classmethod
- def from_string(cls, hash): #pragma: no cover
+ def from_string(cls, hash, **context): #pragma: no cover
"""return parsed instance from hash/configuration string
+ :param \*\*context:
+ context keywords to pass to constructor (if applicable).
+
:raises ValueError: if hash is incorrectly formatted
:returns:
@@ -477,15 +426,12 @@ class GenericHandler(object):
##def to_config_string(self):
## "helper for generating configuration string (ignoring hash)"
- ## chk = self.checksum
- ## if chk:
- ## try:
- ## self.checksum = None
- ## return self.to_string()
- ## finally:
- ## self.checksum = chk
- ## else:
+ ## orig = self.checksum
+ ## try:
+ ## self.checksum = None
## return self.to_string()
+ ## finally:
+ ## self.checksum = orig
#=========================================================
#'crypt-style' interface (default implementation)
@@ -495,8 +441,8 @@ class GenericHandler(object):
return cls(use_defaults=True, **settings).to_string()
@classmethod
- def genhash(cls, secret, config):
- self = cls.from_string(config)
+ def genhash(cls, secret, config, **context):
+ self = cls.from_string(config, **context)
self.checksum = self._calc_checksum(secret)
return self.to_string()
@@ -511,33 +457,171 @@ class GenericHandler(object):
#'application' interface (default implementation)
#=========================================================
@classmethod
- def encrypt(cls, secret, **settings):
- self = cls(use_defaults=True, **settings)
+ def encrypt(cls, secret, **kwds):
+ self = cls(use_defaults=True, **kwds)
self.checksum = self._calc_checksum(secret)
return self.to_string()
@classmethod
- def verify(cls, secret, hash):
- #NOTE: classes with multiple checksum encodings (rare)
- # may wish to either override this, or override _norm_checksum
- # to normalize any checksums provided by from_string()
- self = cls.from_string(hash)
+ def verify(cls, secret, hash, **context):
+ # NOTE: classes with multiple checksum encodings should either
+ # override this method, or ensure that from_string() / _norm_checksum()
+ # ensures .checksum always uses a single canonical representation.
+ self = cls.from_string(hash, **context)
chk = self.checksum
if chk is None:
- raise ValueError("expected %s hash, got %s config string instead" %
- (cls.name, cls.name))
+ raise MissingDigestError(cls)
return consteq(self._calc_checksum(secret), chk)
#=========================================================
+ # undocumented entry points
+ #=========================================================
+
+ ##@classmethod
+ ##def _deprecation_detector(cls, **settings):
+ ## """return helper to detect deprecated hashes.
+ ##
+ ## if this method is defined, the CryptContext constructor
+ ## will invoke it with the settings specified for the context.
+ ## this method should return None or a callable
+ ## with the signature ``func(hash)->bool``.
+ ##
+ ## this function should return true if the hash
+ ## should be re-encrypted, whether due to internal
+ ## issues or the specified settings.
+ ##
+ ## CryptContext will automatically take care of rounds-deprecation
+ ## for GenericHandler-derived classes
+ ## """
+
+ ##@classmethod
+ ##def normhash(cls, hash):
+ ## """helper to clean up non-canonic instances of hash.
+ ## currently only provided by bcrypt() to fix an historical passlib issue.
+ ## """
+
+ #=========================================================
#eoc
#=========================================================
+class StaticHandler(GenericHandler):
+ """GenericHandler mixin for classes which have no settings.
+
+ This mixin assumes the entirety of the hash ise stored in the
+ :attr:`checksum` attribute; that the hash has no rounds, salt,
+ etc. This class provides the following:
+
+ * a default :meth:`genconfig` that always returns None.
+ * a default :meth:`from_string` and :meth:`to_string`
+ that store the entire hash within :attr:`checksum`,
+ after optionally stripping a constant prefix.
+
+ All that is required by subclasses is an implementation of
+ the :meth:`_calc_checksum` method.
+ """
+ # TODO: document _norm_hash()
+
+ setting_kwds = ()
+
+ # optional constant prefix subclasses can specify
+ _hash_prefix = u("")
+
+ @classmethod
+ def from_string(cls, hash, **context):
+ # default from_string() which strips optional prefix,
+ # and passes rest unchanged as checksum value.
+ hash = to_unicode(hash, "ascii", errname="hash")
+ hash = cls._norm_hash(hash)
+ # could enable this for extra strictness
+ ##pat = cls._hash_regex
+ ##if pat and pat.match(hash) is None:
+ ## raise ValueError("not a valid %s hash" % (cls.name,))
+ prefix = cls._hash_prefix
+ if prefix:
+ if hash.startswith(prefix):
+ hash = hash[len(prefix):]
+ else:
+ raise ValueError("not a valid %s hash" % (cls.name,))
+ return cls(checksum=hash, **context)
+
+ @classmethod
+ def _norm_hash(cls, hash):
+ "helper for subclasses to normalize case if needed"
+ return hash
+
+ def to_string(self):
+ assert self.checksum is not None
+ return uascii_to_str(self._hash_prefix + self.checksum)
+
+ @classmethod
+ def genconfig(cls):
+ # since it has no settings, there's no need for a config string.
+ return None
+
+ @classmethod
+ def genhash(cls, secret, config, **context):
+ # since it has no settings, just verify config, and call encrypt()
+ if config is not None and not cls.identify(config):
+ raise ValueError("not a %s hash" % (cls.name,))
+ return cls.encrypt(secret, **context)
+
+ __cc_compat_hack = False
+
+ def _calc_checksum(self, secret): #pragma: no cover
+ """given secret; calcuate and return encoded checksum portion of hash
+ string, taking config from object state
+ """
+ # NOTE: prior to 1.6, StaticHandler required classes implement genhash
+ # instead of this method. so if we reach here, we try calling genhash.
+ # if that succeeds, we issue deprecation warning; if it fails, we'll
+ # recurse back to here, and error will be thrown instead.
+ if not self.__cc_compat_hack:
+ context = dict((k,getattr(self,k)) for k in self.context_kwds)
+ self.__cc_compat_hack = True
+ hash = self.genhash(secret, None, **context)
+ self.__cc_compat_hack = False
+ warn("%r should be updated to implement StaticHandler._calc_checksum() "
+ "instead of StaticHandler.genhash(), support for the latter "
+ "style will be removed in Passlib 1.8" % (self.__class__),
+ DeprecationWarning)
+ return str_to_uascii(hash)
+ else:
+ # else just require subclass to implement this method.
+ raise NotImplementedError("%s must implement _calc_checksum()" %
+ (self.__class__,))
+
#=====================================================
#GenericHandler mixin classes
#=====================================================
+class HasUserContext(GenericHandler):
+ """helper for classes which require a user context keyword"""
+ context_kwds = ("user",)
+
+ def __init__(self, user=None, **kwds):
+ super(HasUserContext, self).__init__(**kwds)
+ self.user = user
+
+ # XXX: would like to validate user input here, but calls to from_string()
+ # which lack context keywords would then fail; so leaving code per-handler.
-#XXX: add a HasContext helper to override GenericHandler's methods?
+ # wrap funcs to accept 'user' as positional arg for ease of use.
+ @classmethod
+ def encrypt(cls, secret, user=None, **context):
+ return super(HasUserContext, cls).encrypt(secret, user=user, **context)
+
+ @classmethod
+ def verify(cls, secret, hash, user=None, **context):
+ return super(HasUserContext, cls).verify(secret, hash, user=user,
+ **context)
+
+ @classmethod
+ def genhash(cls, secret, config, user=None, **context):
+ return super(HasUserContext, cls).genhash(secret, config, user=user,
+ **context)
+#-----------------------------------------------------
+# checksum mixins
+#-----------------------------------------------------
class HasRawChecksum(GenericHandler):
"""mixin for classes which work with decoded checksum bytes
@@ -547,66 +631,14 @@ class HasRawChecksum(GenericHandler):
"""
# NOTE: GenericHandler.checksum_chars is ignored by this implementation.
- def _norm_checksum(self, checksum):
- if checksum is None:
- return None
- if isinstance(checksum, unicode):
- raise TypeError("checksum must be specified as bytes")
- cc = self.checksum_size
- if cc and len(checksum) != cc:
- raise ValueError("checksum wrong size (%s checksum must be "
- "exactly %d characters" % (self.name, cc))
- return checksum
-
-class HasStubChecksum(GenericHandler):
- """modifies class to ignore placeholder checksum used by genconfig().
-
- this is mainly useful for hash formats which don't have a distinguishable
- configuration-only format; and genconfig() has to use a placeholder
- digest (usually all NULLs). this mixin causes that checksum to be
- treated as if there wasn't a checksum at all; preventing the (remote)
- chance of a configuration string 1) being stored as a hash, followed by
- 2) an attacker finding and trying a password which correctly maps to that
- digest.
- """
- _stub_checksum = None
-
- def __init__(self, **kwds):
- super(HasStubChecksum, self).__init__(**kwds)
- chk = self.checksum
- if chk is not None and chk == self._stub_checksum:
- self.checksum = None
-
-#NOTE: commented out because all use-cases work better with StaticHandler
-##class HasNoSettings(GenericHandler):
-## """overrides some GenericHandler methods w/ versions more appropriate for hash w/no settings"""
-##
-## setting_kwds = ()
-##
-## _stub_checksum = None
-##
-## @classmethod
-## def genconfig(cls):
-## if cls._stub_checksum:
-## return cls().to_string()
-## else:
-## return None
-##
-## @classmethod
-## def genhash(cls, secret, config):
-## if config is None and not cls._stub_checksum:
-## self = cls()
-## else:
-## self = cls.from_string(config) #just to validate the input
-## self.checksum = self._calc_checksum(secret)
-## return self.to_string()
-##
-## @classmethod
-## def encrypt(cls, secret):
-## self = cls()
-## self.checksum = self._calc_checksum(secret)
-## return self.to_string()
+ # NOTE: all HasRawChecksum code is currently part of GenericHandler,
+ # using private '_checksum_is_bytes' flag.
+ # this arrangement may be changed in the future.
+ _checksum_is_bytes = True
+#-----------------------------------------------------
+# ident mixins
+#-----------------------------------------------------
class HasManyIdents(GenericHandler):
"""mixin for hashes which use multiple prefix identifiers
@@ -692,10 +724,25 @@ class HasManyIdents(GenericHandler):
return False
return any(hash.startswith(ident) for ident in cls.ident_values)
+ @classmethod
+ def _parse_ident(cls, hash):
+ """extract ident prefix from hash, helper for subclasses' from_string()"""
+ if not hash:
+ raise ValueError("no hash specified")
+ if isinstance(hash, bytes):
+ hash = hash.decode("ascii")
+ for ident in cls.ident_values:
+ if hash.startswith(ident):
+ return ident, hash[len(ident):]
+ raise ValueError("invalid %s hash" % (cls.name,))
+
#=========================================================
#eoc
#=========================================================
+#-----------------------------------------------------
+# salt mixins
+#-----------------------------------------------------
class HasSalt(GenericHandler):
"""mixin for validating salts.
@@ -766,7 +813,8 @@ class HasSalt(GenericHandler):
.. automethod:: _norm_salt
.. automethod:: _generate_salt
"""
- #XXX: allow providing raw salt to this class, and encoding it?
+ # TODO: document _truncate_salt()
+ # XXX: allow providing raw salt to this class, and encoding it?
#=========================================================
#class attrs
@@ -839,6 +887,7 @@ class HasSalt(GenericHandler):
raise TypeError("salt must be specified as bytes")
else:
if not isinstance(salt, unicode):
+ # XXX: should we disallow bytes here?
if isinstance(salt, bytes):
salt = salt.decode("ascii")
else:
@@ -846,12 +895,8 @@ class HasSalt(GenericHandler):
# check charset
sc = self.salt_chars
- if sc is not None:
- bad = set(salt)
- bad.difference_update(sc)
- if bad:
- raise ValueError("invalid characters in %s salt: %r" %
- (self.name, ujoin(sorted(bad))))
+ if sc is not None and any(c not in sc for c in salt):
+ raise ValueError("invalid characters in %s salt" % self.name)
# check min size
mn = self.min_salt_size
@@ -904,9 +949,8 @@ class HasRawSalt(HasSalt):
salt_chars = ALL_BYTE_VALUES
- #NOTE: all HasRawSalt code is currently part of HasSalt,
- # using private _salt_is_bytes flag.
- # this arrangement may be changed in the future.
+ # NOTE: all HasRawSalt code is currently part of HasSalt, using private
+ # '_salt_is_bytes' flag. this arrangement may be changed in the future.
_salt_is_bytes = True
_salt_unit = "bytes"
@@ -914,6 +958,9 @@ class HasRawSalt(HasSalt):
assert self.salt_chars in [None, ALL_BYTE_VALUES]
return getrandbytes(rng, salt_size)
+#-----------------------------------------------------
+# rounds mixin
+#-----------------------------------------------------
class HasRounds(GenericHandler):
"""mixin for validating rounds parameter
@@ -1043,6 +1090,9 @@ class HasRounds(GenericHandler):
#eoc
#=========================================================
+#-----------------------------------------------------
+# backend mixin & helpers
+#-----------------------------------------------------
def _clear_backend(cls):
"restore HasManyBackend subclass to unloaded state - used by unittests"
assert issubclass(cls, HasManyBackends) and cls is not HasManyBackends
@@ -1138,11 +1188,10 @@ class HasManyBackends(GenericHandler):
``True`` if backend is currently supported, else ``False``.
"""
if name in ("any", "default"):
- try:
- cls.set_backend()
+ if name == "any" and cls._backend:
return True
- except MissingBackendError:
- return False
+ return any(getattr(cls, "_has_backend_" + name)
+ for name in cls.backends)
elif name in cls.backends:
return getattr(cls, "_has_backend_" + name)
else:
@@ -1198,7 +1247,8 @@ class HasManyBackends(GenericHandler):
else:
raise MissingBackendError(cls._no_backends_msg())
elif not cls.has_backend(name):
- raise MissingBackendError("%s backend not available: %r" % (cls.name, name))
+ raise MissingBackendError("%s backend not available: %r" %
+ (cls.name, name))
cls._calc_checksum = getattr(cls, "_calc_checksum_" + name)
cls._backend = name
return name
@@ -1325,6 +1375,8 @@ class PrefixWrapper(object):
_proxy_attrs = (
"setting_kwds", "context_kwds",
"default_rounds", "min_rounds", "max_rounds", "rounds_cost",
+ "default_salt_size", "min_salt_size", "max_salt_size",
+ "salt_chars", "default_salt_chars",
"backends", "has_backend", "get_backend", "set_backend",
)