summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES8
-rw-r--r--admin/benchmarks.py4
-rw-r--r--docs/password_hash_api.rst19
-rw-r--r--passlib/context.py42
-rw-r--r--passlib/exc.py14
-rw-r--r--passlib/ext/django/models.py4
-rw-r--r--passlib/handlers/bcrypt.py12
-rw-r--r--passlib/handlers/cisco.py12
-rw-r--r--passlib/handlers/des_crypt.py22
-rw-r--r--passlib/handlers/digests.py5
-rw-r--r--passlib/handlers/django.py18
-rw-r--r--passlib/handlers/fshp.py6
-rw-r--r--passlib/handlers/ldap_digests.py24
-rw-r--r--passlib/handlers/misc.py46
-rw-r--r--passlib/handlers/mssql.py27
-rw-r--r--passlib/handlers/mysql.py8
-rw-r--r--passlib/handlers/oracle.py9
-rw-r--r--passlib/handlers/pbkdf2.py9
-rw-r--r--passlib/handlers/postgres.py3
-rw-r--r--passlib/handlers/scram.py8
-rw-r--r--passlib/handlers/sha2_crypt.py8
-rw-r--r--passlib/handlers/sun_md5_crypt.py17
-rw-r--r--passlib/handlers/windows.py16
-rw-r--r--passlib/tests/test_context.py66
-rw-r--r--passlib/tests/test_utils_handlers.py2
-rw-r--r--passlib/tests/utils.py65
-rw-r--r--passlib/utils/__init__.py21
-rw-r--r--passlib/utils/compat.py11
-rw-r--r--passlib/utils/handlers.py95
-rw-r--r--passlib/utils/pbkdf2.py2
30 files changed, 298 insertions, 305 deletions
diff --git a/CHANGES b/CHANGES
index c329398..866d845 100644
--- a/CHANGES
+++ b/CHANGES
@@ -84,6 +84,14 @@ Release History
legacy config files may need to escape raw ``%`` characters
in order to load successfully.
+ * The main CryptContext methods (e.g. :meth:`~CryptContext.encrypt`,
+ and :meth:`~CryptContext.verify`) will now consistently raise
+ a :exc:`TypeError` when called with ``hash=None`` or another
+ non-string type, to match the :doc:`password-hash-api`.
+ Under previous releases, they might return ``False``,
+ raise :exc:`ValueError`, or raise :exc:`TypeError`,
+ depending on the specific method and context settings.
+
Utils
.. currentmodule:: passlib.utils.handlers
diff --git a/admin/benchmarks.py b/admin/benchmarks.py
index 91c58e1..5cba45e 100644
--- a/admin/benchmarks.py
+++ b/admin/benchmarks.py
@@ -55,8 +55,8 @@ class BlankHandler(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
def to_string(self):
return uh.render_mc3(self.ident, self.rounds, self.salt, self.checksum)
- def _calc_checksum(self, password):
- return unicode(password[0:1])
+ def _calc_checksum(self, secret):
+ return unicode(secret[0:1])
class AnotherHandler(BlankHandler):
name = "another"
diff --git a/docs/password_hash_api.rst b/docs/password_hash_api.rst
index 67f9ba6..c349992 100644
--- a/docs/password_hash_api.rst
+++ b/docs/password_hash_api.rst
@@ -216,7 +216,7 @@ which scheme a hash belongs to when multiple schemes are in use.
:raises TypeError:
- * if :samp:`{secret}` is not a bytes or unicode instance.
+ * if :samp:`{secret}` is not a unicode or bytes instance.
* if a required option (such as a context keyword) was not set.
@@ -237,8 +237,9 @@ which scheme a hash belongs to when multiple schemes are in use.
Quickly identify if a hash string belongs to this algorithm.
- :arg hash:
- the candidate hash string to check
+ :arg hash: the candidate hash string to check
+
+ :raises TypeError: if :samp:`{hash}` is not a unicode or bytes instance.
:returns:
``True`` if the input appears to be a hash or configuration string
@@ -273,11 +274,12 @@ which scheme a hash belongs to when multiple schemes are in use.
method. These should be limited to those listed
in :attr:`~PasswordHash.context_kwds`.
- :raises TypeError: if :samp:`{secret}` is not a bytes or unicode instance.
+ :raises TypeError:
+
+ if either *secret* or *hash* is not a unicode or bytes instance.
:raises ValueError:
- * if no hash is provided, or the hash does not match this
- algorithm's hash format.
+ * the hash does not match this algorithm's hash format.
* if the secret contains forbidden characters (see
:meth:`~PasswordHash.encrypt`).
* if a configuration string from :meth:`~PasswordHash.genconfig`
@@ -356,9 +358,8 @@ and :meth:`~PasswordHash.genhash`.
these kwds must be specified in :attr:`~PasswordHash.context_kwds`.
:raises TypeError:
- * if the configuration string is not provided
- * if required contextual information is not provided
- * if :samp:`{secret}` is not a bytes or unicode instance.
+ * if either *secret* or *config* is not a unicode or bytes instance.
+ * if required contextual keywords are not provided
:raises ValueError:
* if the configuration string is not in a recognized format.
diff --git a/passlib/context.py b/passlib/context.py
index e5667c1..e5bc2c7 100644
--- a/passlib/context.py
+++ b/passlib/context.py
@@ -16,13 +16,13 @@ from time import sleep
from warnings import warn
#site
#libs
-from passlib.exc import PasslibConfigWarning
+from passlib.exc import PasslibConfigWarning, ExpectedStringError
from passlib.registry import get_crypt_handler, _validate_handler_name
from passlib.utils import is_crypt_handler, rng, saslprep, tick, to_bytes, \
to_unicode
from passlib.utils.compat import bytes, is_mapping, iteritems, num_types, \
PY3, PY_MIN_32, unicode, SafeConfigParser, \
- NativeStringIO, BytesIO
+ NativeStringIO, BytesIO, base_string_types
#pkg
#local
__all__ = [
@@ -1311,18 +1311,20 @@ class CryptContext(object):
]
return value
- def _identify_record(self, hash, category=None, required=True):
- "internal helper to identify appropriate _HandlerRecord"
+ def _identify_record(self, hash, category, required=True):
+ """internal helper to identify appropriate _CryptRecord for hash"""
+ if not isinstance(hash, base_string_types):
+ raise ExpectedStringError(hash, "hash")
records = self._get_record_list(category)
for record in records:
if record.identify(hash):
return record
- if required:
- if not records:
- raise KeyError("no crypt algorithms supported")
- raise ValueError("hash could not be identified")
- else:
+ if not required:
return None
+ elif not records:
+ raise KeyError("no crypt algorithms supported")
+ else:
+ raise ValueError("hash could not be identified")
#===================================================================
#password hash api proxy methods
@@ -1334,8 +1336,8 @@ class CryptContext(object):
# since it will have optimized itself for the particular
# settings used within the policy by that (scheme,category).
- # XXX: would a better name be is_deprecated(hash)?
- def hash_needs_update(self, hash, category=None):
+ # XXX: would a better name be needs_update/is_deprecated?
+ def hash_needs_update(self, hash, scheme=None, category=None):
"""check if hash is allowed by current policy, or if secret should be re-encrypted.
the core of CryptContext's support for hash migration:
@@ -1347,12 +1349,18 @@ class CryptContext(object):
if so, the password should be re-encrypted using ``ctx.encrypt(passwd)``.
:arg hash: existing hash string
+ :param scheme: optionally identify specific scheme to check against.
:param category: optional user category
:returns: True/False
"""
- # XXX: add scheme kwd for compatibility w/ other methods?
- return self._identify_record(hash, category).hash_needs_update(hash)
+ if scheme:
+ if not isinstance(hash, base_string_types):
+ raise ExpectedStringError(hash, "hash")
+ record = self._get_record(scheme, category)
+ else:
+ record = self._identify_record(hash, category)
+ return record.hash_needs_update(hash)
def genconfig(self, scheme=None, category=None, **settings):
"""Call genconfig() for specified handler
@@ -1395,10 +1403,6 @@ class CryptContext(object):
The handler which first identifies the hash,
or ``None`` if none of the algorithms identify the hash.
"""
- if hash is None:
- if required:
- raise ValueError("no hash provided")
- return None
record = self._identify_record(hash, category, required)
if record is None:
return None
@@ -1456,8 +1460,6 @@ class CryptContext(object):
:returns: True/False
"""
- if hash is None:
- return False
if scheme:
record = self._get_record(scheme, category)
else:
@@ -1505,8 +1507,6 @@ class CryptContext(object):
.. seealso:: :ref:`context-migrating-passwords` for a usage example.
"""
- if hash is None:
- return False, None
if scheme:
record = self._get_record(scheme, category)
else:
diff --git a/passlib/exc.py b/passlib/exc.py
index 70347ee..7c2bd30 100644
--- a/passlib/exc.py
+++ b/passlib/exc.py
@@ -102,9 +102,17 @@ def _get_name(handler):
#----------------------------------------------------------------
# encrypt/verify parameter errors
#----------------------------------------------------------------
-def MissingHashError(handler=None):
- "error raised if no hash provided to handler"
- return ValueError("no hash specified")
+def ExpectedStringError(value, param):
+ "error message when param was supposed to be unicode or bytes"
+ # NOTE: value is never displayed, since it may sometimes be a password.
+ cls = value.__class__
+ if cls.__module__ and cls.__module__ != "__builtin__":
+ name = "%s.%s" % (cls.__module__, cls.__name__)
+ elif value is None:
+ name = 'None'
+ else:
+ name = cls.__name__
+ return TypeError("%s must be unicode or bytes, not %s" % (param, name))
def MissingDigestError(handler=None):
"raised when verify() method gets passed config string instead of hash"
diff --git a/passlib/ext/django/models.py b/passlib/ext/django/models.py
index 2b3dd16..edc334a 100644
--- a/passlib/ext/django/models.py
+++ b/passlib/ext/django/models.py
@@ -15,7 +15,7 @@ from django.conf import settings
#pkg
from passlib.context import CryptContext, CryptPolicy
from passlib.utils import is_crypt_context
-from passlib.utils.compat import bytes, sb_types, unicode
+from passlib.utils.compat import bytes, unicode, base_string_types
from passlib.ext.django.utils import DEFAULT_CTX, get_category, \
set_django_password_context
@@ -34,7 +34,7 @@ def patch():
return
if ctx == "passlib-default":
ctx = DEFAULT_CTX
- if isinstance(ctx, str):
+ if isinstance(ctx, base_string_types):
ctx = CryptContext(policy=CryptPolicy.from_string(ctx))
if not is_crypt_context(ctx):
raise TypeError("django settings.PASSLIB_CONTEXT must be CryptContext "
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py
index c1c4127..6d03c98 100644
--- a/passlib/handlers/bcrypt.py
+++ b/passlib/handlers/bcrypt.py
@@ -126,13 +126,13 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
if ident == IDENT_2X:
raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
"currently supported")
- rounds, data = tail.split(u("$"))
- rval = int(rounds)
- if rounds != u('%02d') % (rval,):
- raise uh.exc.ZeroPaddedRoundsError(cls)
+ rounds_str, data = tail.split(u("$"))
+ rounds = int(rounds_str)
+ if rounds_str != u('%02d') % (rounds,):
+ raise uh.exc.MalformedHashError(cls, "malformed cost field")
salt, chk = data[:22], data[22:]
return cls(
- rounds=rval,
+ rounds=rounds,
salt=salt,
checksum=chk or None,
ident=ident,
@@ -289,8 +289,6 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh.
return str_to_uascii(hash[-31:])
def _calc_checksum_builtin(self, secret):
- if secret is None:
- raise TypeError("no secret provided")
warn("SECURITY WARNING: Passlib is using it's pure-python bcrypt "
"implementation, which is TOO SLOW FOR PRODUCTION USE. It is "
"strongly recommended that you install py-bcrypt or bcryptor for "
diff --git a/passlib/handlers/cisco.py b/passlib/handlers/cisco.py
index 28c02f2..b4519a9 100644
--- a/passlib/handlers/cisco.py
+++ b/passlib/handlers/cisco.py
@@ -10,7 +10,7 @@ from warnings import warn
#site
#libs
#pkg
-from passlib.utils import h64, to_bytes, right_pad_string
+from passlib.utils import h64, right_pad_string, to_unicode
from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, join_byte_values, \
join_byte_elems, byte_elem_value, iter_byte_values, uascii_to_str, str_to_uascii
import passlib.utils.handlers as uh
@@ -121,14 +121,9 @@ class cisco_type7(uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- if hash is None:
- return cls(use_defaults=True)
- raise uh.exc.MissingHashError(cls)
+ hash = to_unicode(hash, "ascii", "hash")
if len(hash) < 2:
raise uh.exc.InvalidHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("latin-1")
salt = int(hash[:2]) # may throw ValueError
return cls(salt=salt, checksum=hash[2:].upper())
@@ -165,7 +160,8 @@ class cisco_type7(uh.GenericHandler):
def _calc_checksum(self, secret):
# XXX: no idea what unicode policy is, but all examples are
# 7-bit ascii compatible, so using UTF-8
- secret = to_bytes(secret, "utf-8", errname="secret")
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
return hexlify(self._cipher(secret, self.salt)).decode("ascii").upper()
@classmethod
diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py
index df7683a..9517899 100644
--- a/passlib/handlers/des_crypt.py
+++ b/passlib/handlers/des_crypt.py
@@ -58,7 +58,7 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import classproperty, h64, h64big, safe_crypt, test_crypt
+from passlib.utils import classproperty, h64, h64big, safe_crypt, test_crypt, to_unicode
from passlib.utils.compat import b, bytes, byte_elem_value, u, uascii_to_str, unicode
from passlib.utils.des import mdes_encrypt_int_block
import passlib.utils.handlers as uh
@@ -182,10 +182,7 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
salt, chk = hash[:2], hash[2:]
return cls(salt=salt, checksum=chk or None)
@@ -296,10 +293,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
@@ -383,10 +377,7 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
@@ -463,10 +454,7 @@ class crypt16(uh.HasSalt, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
diff --git a/passlib/handlers/digests.py b/passlib/handlers/digests.py
index 03db62c..ec08056 100644
--- a/passlib/handlers/digests.py
+++ b/passlib/handlers/digests.py
@@ -9,7 +9,7 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import to_native_str, to_bytes
+from passlib.utils import to_native_str
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
@@ -44,7 +44,8 @@ class HexDigestHash(uh.StaticHandler):
return hash.lower()
def _calc_checksum(self, secret):
- secret = to_bytes(secret, "utf-8", errname="secret")
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
return str_to_uascii(self._hash_func(secret).hexdigest())
#=========================================================
diff --git a/passlib/handlers/django.py b/passlib/handlers/django.py
index 9a22a38..d0c7e11 100644
--- a/passlib/handlers/django.py
+++ b/passlib/handlers/django.py
@@ -49,10 +49,7 @@ class DjangoSaltedHash(uh.HasSalt, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
ident = cls.ident
assert ident.endswith(u("$"))
if not hash.startswith(ident):
@@ -201,22 +198,15 @@ class django_disabled(uh.StaticHandler):
@classmethod
def identify(cls, hash):
- if not hash:
- return False
- if isinstance(hash, bytes):
- return hash == b("!")
- else:
- return hash == u("!")
+ hash = uh.to_unicode_for_identify(hash)
+ return hash == u("!")
def _calc_checksum(self, secret):
- if secret is None:
- raise TypeError("no secret provided")
return u("!")
@classmethod
def verify(cls, secret, hash):
- if secret is None:
- raise TypeError("no secret provided")
+ uh.validate_secret(secret)
if not cls.identify(hash):
raise uh.exc.InvalidHashError(cls)
return False
diff --git a/passlib/handlers/fshp.py b/passlib/handlers/fshp.py
index eb6fcfd..3404bd8 100644
--- a/passlib/handlers/fshp.py
+++ b/passlib/handlers/fshp.py
@@ -11,6 +11,7 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
+from passlib.utils import to_unicode
import passlib.utils.handlers as uh
from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, u,\
unicode
@@ -141,10 +142,7 @@ class fshp(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
diff --git a/passlib/handlers/ldap_digests.py b/passlib/handlers/ldap_digests.py
index 4acbd4c..19089ee 100644
--- a/passlib/handlers/ldap_digests.py
+++ b/passlib/handlers/ldap_digests.py
@@ -12,8 +12,8 @@ from warnings import warn
#site
#libs
from passlib.handlers.misc import plaintext
-from passlib.utils import to_native_str, unix_crypt_schemes, to_bytes, \
- classproperty
+from passlib.utils import to_native_str, unix_crypt_schemes, \
+ classproperty, to_unicode
from passlib.utils.compat import b, bytes, uascii_to_str, unicode, u
import passlib.utils.handlers as uh
#pkg
@@ -53,7 +53,8 @@ class _Base64DigestHelper(uh.StaticHandler):
return cls.ident
def _calc_checksum(self, secret):
- secret = to_bytes(secret, "utf-8", errname="secret")
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
chk = self._hash_func(secret).digest()
return b64encode(chk).decode("ascii")
@@ -78,10 +79,7 @@ class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHand
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode('ascii')
+ hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
@@ -99,8 +97,6 @@ class _SaltedBase64DigestHelper(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHand
return uascii_to_str(hash)
def _calc_checksum(self, secret):
- if secret is None:
- raise TypeError("no secret provided")
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
return self._hash_func(secret + self.salt).digest()
@@ -199,15 +195,9 @@ class ldap_plaintext(plaintext):
@classmethod
def identify(cls, hash):
- if not hash:
- return False
- if isinstance(hash, bytes):
- try:
- hash = hash.decode(cls._hash_encoding)
- except UnicodeDecodeError:
- return False
# NOTE: identifies all strings EXCEPT those with {XXX} prefix
- return cls._2307_pat.match(hash) is None
+ hash = uh.to_unicode_for_identify(hash)
+ return bool(hash) and cls._2307_pat.match(hash) is None
#=========================================================
#{CRYPT} wrappers
diff --git a/passlib/handlers/misc.py b/passlib/handlers/misc.py
index e63ea1b..cb812ff 100644
--- a/passlib/handlers/misc.py
+++ b/passlib/handlers/misc.py
@@ -10,7 +10,7 @@ from warnings import warn
#site
#libs
from passlib.utils import to_native_str, consteq
-from passlib.utils.compat import bytes, unicode, u
+from passlib.utils.compat import bytes, unicode, u, base_string_types
import passlib.utils.handlers as uh
#pkg
#local
@@ -48,7 +48,10 @@ class unix_fallback(uh.StaticHandler):
@classmethod
def identify(cls, hash):
- return hash is not None
+ if isinstance(hash, base_string_types):
+ return True
+ else:
+ raise uh.exc.ExpectedStringError(hash, "hash")
def __init__(self, enable_wildcard=False, **kwds):
warn("'unix_fallback' is deprecated, "
@@ -59,8 +62,6 @@ class unix_fallback(uh.StaticHandler):
self.enable_wildcard = enable_wildcard
def _calc_checksum(self, secret):
- if secret is None:
- raise TypeError("secret must be string")
if self.checksum:
# NOTE: hash will generally be "!", but we want to preserve
# it in case it's something else, like "*".
@@ -70,10 +71,9 @@ class unix_fallback(uh.StaticHandler):
@classmethod
def verify(cls, secret, hash, enable_wildcard=False):
- if secret is None:
- raise TypeError("secret must be string")
- elif hash is None:
- raise uh.exc.MissingHashError(cls)
+ uh.validate_secret(secret)
+ if not isinstance(hash, base_string_types):
+ raise uh.exc.ExpectedStringError(hash, "hash")
elif hash:
return False
else:
@@ -114,7 +114,10 @@ class unix_disabled(object):
@classmethod
def identify(cls, hash):
- return hash is not None
+ if isinstance(hash, base_string_types):
+ return True
+ else:
+ raise uh.exc.ExpectedStringError(hash, "hash")
@classmethod
def encrypt(cls, secret, marker=None):
@@ -122,10 +125,9 @@ class unix_disabled(object):
@classmethod
def verify(cls, secret, hash):
- if secret is None:
- raise TypeError("no secret provided")
- if hash is None:
- raise TypeError("no hash provided")
+ uh.validate_secret(secret)
+ if not isinstance(hash, base_string_types):
+ raise uh.exc.ExpectedStringError(hash, "hash")
return False
@classmethod
@@ -134,8 +136,7 @@ class unix_disabled(object):
@classmethod
def genhash(cls, secret, config, marker=None):
- if secret is None:
- raise TypeError("secret must be string")
+ uh.validate_secret(secret)
if config is not None:
# NOTE: config/hash will generally be "!" or "*",
# but we want to preserve it in case it has some other content,
@@ -165,22 +166,21 @@ class plaintext(object):
@classmethod
def identify(cls, hash):
- # by default, identify ALL strings
- return hash is not None
+ if isinstance(hash, base_string_types):
+ return True
+ else:
+ raise uh.exc.ExpectedStringError(hash, "hash")
@classmethod
def encrypt(cls, secret):
- if secret and len(secret) > uh.MAX_PASSWORD_SIZE:
- raise uh.exc.PasswordSizeError()
+ uh.validate_secret(secret)
return to_native_str(secret, cls._hash_encoding, "secret")
@classmethod
def verify(cls, secret, hash):
- if hash is None:
- raise TypeError("no hash specified")
- elif not cls.identify(hash):
- raise uh.exc.InvalidHashError(cls)
hash = to_native_str(hash, cls._hash_encoding, "hash")
+ if not cls.identify(hash):
+ raise uh.exc.InvalidHashError(cls)
return consteq(cls.encrypt(secret), hash)
@classmethod
diff --git a/passlib/handlers/mssql.py b/passlib/handlers/mssql.py
index eafd44a..e46c665 100644
--- a/passlib/handlers/mssql.py
+++ b/passlib/handlers/mssql.py
@@ -43,7 +43,7 @@ from warnings import warn
#site
#libs
#pkg
-from passlib.utils import to_unicode, consteq
+from passlib.utils import consteq
from passlib.utils.compat import b, bytes, bascii_to_str, unicode, u
import passlib.utils.handlers as uh
#local
@@ -66,30 +66,27 @@ UIDENT = u("0x0100")
def _ident_mssql(hash, csize, bsize):
"common identify for mssql 2000/2005"
- if not hash:
- return False
if isinstance(hash, unicode):
if len(hash) == csize and hash.startswith(UIDENT):
return True
- else:
- assert isinstance(hash, bytes)
+ elif isinstance(hash, bytes):
if len(hash) == csize and hash.startswith(BIDENT):
return True
##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes
## return True
+ else:
+ raise uh.exc.ExpectedStringError(hash, "hash")
return False
def _parse_mssql(hash, csize, bsize, handler):
"common parser for mssql 2000/2005; returns 4 byte salt + checksum"
- if not hash:
- raise uh.exc.MissingHashError(handler)
if isinstance(hash, unicode):
if len(hash) == csize and hash.startswith(UIDENT):
try:
return unhexlify(hash[6:].encode("utf-8"))
except TypeError: # throw when bad char found
pass
- else:
+ elif isinstance(hash, bytes):
# assumes ascii-compat encoding
assert isinstance(hash, bytes)
if len(hash) == csize and hash.startswith(BIDENT):
@@ -99,6 +96,8 @@ def _parse_mssql(hash, csize, bsize, handler):
pass
##elif len(hash) == bsize and hash.startswith(BIDENT2): # raw bytes
## return hash[2:]
+ else:
+ raise uh.exc.ExpectedStringError(hash, "hash")
raise uh.exc.InvalidHashError(handler)
class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
@@ -148,7 +147,8 @@ class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
return "0x0100" + bascii_to_str(hexlify(raw).upper())
def _calc_checksum(self, secret):
- secret = to_unicode(secret, 'utf-8', errname='secret')
+ if isinstance(secret, bytes):
+ secret = secret.decode("utf-8")
salt = self.salt
return _raw_mssql(secret, salt) + _raw_mssql(secret.upper(), salt)
@@ -156,13 +156,13 @@ class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
def verify(cls, secret, hash):
# NOTE: we only compare against the upper-case hash
# XXX: add 'full' just to verify both checksums?
+ uh.validate_secret(secret)
self = cls.from_string(hash)
chk = self.checksum
if chk is None:
raise uh.exc.MissingDigestError(cls)
- if secret and len(secret) > uh.MAX_PASSWORD_SIZE:
- raise uh.exc.PasswordSizeError()
- secret = to_unicode(secret, 'utf-8', errname='secret')
+ if isinstance(secret, bytes):
+ secret = secret.decode("utf-8")
result = _raw_mssql(secret.upper(), self.salt)
return consteq(result, chk[20:])
@@ -216,7 +216,8 @@ class mssql2005(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
return "0x0100" + bascii_to_str(hexlify(raw)).upper()
def _calc_checksum(self, secret):
- secret = to_unicode(secret, 'utf-8', errname='secret')
+ if isinstance(secret, bytes):
+ secret = secret.decode("utf-8")
return _raw_mssql(secret, self.salt)
#=========================================================
diff --git a/passlib/handlers/mysql.py b/passlib/handlers/mysql.py
index 7bbaeb2..9cb4eeb 100644
--- a/passlib/handlers/mysql.py
+++ b/passlib/handlers/mysql.py
@@ -30,7 +30,7 @@ from warnings import warn
#site
#libs
#pkg
-from passlib.utils import to_native_str, to_bytes
+from passlib.utils import to_native_str
from passlib.utils.compat import b, bascii_to_str, bytes, unicode, u, \
byte_elem_value, str_to_uascii
import passlib.utils.handlers as uh
@@ -66,7 +66,8 @@ class mysql323(uh.StaticHandler):
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")
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
MASK_32 = 0xffffffff
MASK_31 = 0x7fffffff
@@ -115,7 +116,8 @@ class mysql41(uh.StaticHandler):
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")
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
return str_to_uascii(sha1(sha1(secret).digest()).hexdigest()).upper()
#=========================================================
diff --git a/passlib/handlers/oracle.py b/passlib/handlers/oracle.py
index 9a0af1b..24ef319 100644
--- a/passlib/handlers/oracle.py
+++ b/passlib/handlers/oracle.py
@@ -88,8 +88,8 @@ class oracle10(uh.HasUserContext, uh.StaticHandler):
#
# this whole mess really needs someone w/ an oracle system,
# and some answers :)
-
- secret = to_unicode(secret, "utf-8", errname="secret")
+ if isinstance(secret, bytes):
+ secret = secret.decode("utf-8")
user = to_unicode(self.user, "utf-8", errname="user")
input = (user+secret).upper().encode("utf-16-be")
hash = des_cbc_encrypt(ORACLE10_MAGIC, input)
@@ -138,10 +138,7 @@ class oracle11(uh.HasSalt, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
m = cls._hash_regex.match(hash)
if not m:
raise uh.exc.InvalidHashError(cls)
diff --git a/passlib/handlers/pbkdf2.py b/passlib/handlers/pbkdf2.py
index 191e673..662bdcd 100644
--- a/passlib/handlers/pbkdf2.py
+++ b/passlib/handlers/pbkdf2.py
@@ -10,7 +10,7 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import ab64_decode, ab64_encode
+from passlib.utils import ab64_decode, ab64_encode, to_unicode
from passlib.utils.compat import b, bytes, str_to_bascii, u, uascii_to_str, unicode
from passlib.utils.pbkdf2 import pbkdf2
import passlib.utils.handlers as uh
@@ -279,8 +279,6 @@ class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
rounds, salt, chk = uh.parse_mc3(hash, cls.ident, rounds_base=16,
default_rounds=400, handler=cls)
return cls(rounds=rounds, salt=salt, checksum=chk)
@@ -335,10 +333,7 @@ class atlassian_pbkdf2_sha1(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler)
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
ident = cls.ident
if not hash.startswith(ident):
raise uh.exc.InvalidHashError(cls)
diff --git a/passlib/handlers/postgres.py b/passlib/handlers/postgres.py
index 63e7ddd..c794c19 100644
--- a/passlib/handlers/postgres.py
+++ b/passlib/handlers/postgres.py
@@ -45,7 +45,8 @@ class postgres_md5(uh.HasUserContext, uh.StaticHandler):
# primary interface
#=========================================================
def _calc_checksum(self, secret):
- secret = to_bytes(secret, "utf-8", errname="secret")
+ if isinstance(secret, unicode):
+ secret = secret.encode("utf-8")
user = to_bytes(self.user, "utf-8", errname="user")
return str_to_uascii(md5(secret + user).hexdigest())
diff --git a/passlib/handlers/scram.py b/passlib/handlers/scram.py
index 25ff036..e7919a2 100644
--- a/passlib/handlers/scram.py
+++ b/passlib/handlers/scram.py
@@ -209,10 +209,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
@classmethod
def from_string(cls, hash):
- # parse hash
- if not hash:
- raise uh.exc.MissingHashError(cls)
- hash = to_native_str(hash, "ascii", errname="hash")
+ hash = to_native_str(hash, "ascii", "hash")
if not hash.startswith("$scram$"):
raise uh.exc.InvalidHashError(cls)
parts = hash[7:].split("$")
@@ -351,8 +348,7 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
@classmethod
def verify(cls, secret, hash, full=False):
- if secret and len(secret) > uh.MAX_PASSWORD_SIZE:
- raise uh.exc.PasswordSizeError()
+ uh.validate_secret(secret)
self = cls.from_string(hash)
chkmap = self.checksum
if not chkmap:
diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py
index f009479..e344912 100644
--- a/passlib/handlers/sha2_crypt.py
+++ b/passlib/handlers/sha2_crypt.py
@@ -8,7 +8,8 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import classproperty, h64, safe_crypt, test_crypt, repeat_string
+from passlib.utils import classproperty, h64, safe_crypt, test_crypt, \
+ repeat_string, to_unicode
from passlib.utils.compat import b, bytes, byte_elem_value, irange, u, \
uascii_to_str, unicode, lmap
import passlib.utils.handlers as uh
@@ -279,10 +280,7 @@ class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt,
# portion has a slightly different grammar.
# convert to unicode, check for ident prefix, split on dollar signs.
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode('ascii')
+ hash = to_unicode(hash, "ascii", "hash")
ident = cls.ident
if not hash.startswith(ident):
raise uh.exc.InvalidHashError(cls)
diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py
index e1d187b..0349dea 100644
--- a/passlib/handlers/sun_md5_crypt.py
+++ b/passlib/handlers/sun_md5_crypt.py
@@ -17,7 +17,7 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import h64
+from passlib.utils import h64, to_unicode
from passlib.utils.compat import b, bytes, byte_elem_value, irange, u, \
uascii_to_str, unicode, str_to_bascii
import passlib.utils.handlers as uh
@@ -235,21 +235,12 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
#=========================================================
@classmethod
def identify(cls, hash):
- if not hash:
- return False
- if isinstance(hash, bytes):
- try:
- hash = hash.decode("ascii")
- except UnicodeDecodeError:
- return False
+ hash = uh.to_unicode_for_identify(hash)
return hash.startswith(cls.ident_values)
@classmethod
def from_string(cls, hash):
- if not hash:
- raise uh.exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
#
#detect if hash specifies rounds value.
@@ -338,8 +329,6 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler):
def _calc_checksum(self, secret):
#NOTE: no reference for how sun_md5_crypt handles unicode
- if secret is None:
- raise TypeError("no secret specified")
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
config = str_to_bascii(self.to_string(withchk=False))
diff --git a/passlib/handlers/windows.py b/passlib/handlers/windows.py
index d522755..fc77d40 100644
--- a/passlib/handlers/windows.py
+++ b/passlib/handlers/windows.py
@@ -9,7 +9,7 @@ import logging; log = logging.getLogger(__name__)
from warnings import warn
#site
#libs
-from passlib.utils import to_unicode, to_bytes, right_pad_string
+from passlib.utils import to_unicode, right_pad_string
from passlib.utils.compat import b, bytes, str_to_uascii, u, unicode, uascii_to_str
from passlib.utils.md4 import md4
import passlib.utils.handlers as uh
@@ -185,13 +185,8 @@ bsd_nthash = uh.PrefixWrapper("bsd_nthash", nthash, prefix="$3$$", ident="$3$$",
##
## @classmethod
## def identify(cls, hash):
-## if not hash:
-## return False
-## if isinstance(hash, bytes):
-## hash = hash.decode("latin-1")
-## if len(hash) != 65:
-## return False
-## return cls._hash_regex.match(hash) is not None
+## hash = to_unicode(hash, "latin-1", "hash")
+## return len(hash) == 65 and cls._hash_regex.match(hash) is not None
##
## @classmethod
## def genconfig(cls):
@@ -209,10 +204,7 @@ bsd_nthash = uh.PrefixWrapper("bsd_nthash", nthash, prefix="$3$$", ident="$3$$",
##
## @classmethod
## def verify(cls, secret, hash):
-## if hash is None:
-## raise TypeError("no hash specified")
-## if isinstance(hash, bytes):
-## hash = hash.decode("latin-1")
+## hash = to_unicode(hash, "ascii", "hash")
## m = cls._hash_regex.match(hash)
## if not m:
## raise uh.exc.InvalidHashError(cls)
diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py
index f9edafc..373d066 100644
--- a/passlib/tests/test_context.py
+++ b/passlib/tests/test_context.py
@@ -521,7 +521,7 @@ admin__context__deprecated = des_crypt, bsdi_crypt
#CryptContext
#=========================================================
class CryptContextTest(TestCase):
- "test CryptContext object's behavior"
+ "test CryptContext class"
descriptionPrefix = "CryptContext"
#=========================================================
@@ -893,10 +893,6 @@ class CryptContextTest(TestCase):
self.assertEqual(cc.identify('$9$232323123$1287319827'), None)
self.assertRaises(ValueError, cc.identify, '$9$232323123$1287319827', required=True)
- #make sure "None" is accepted
- self.assertEqual(cc.identify(None), None)
- self.assertRaises(ValueError, cc.identify, None, required=True)
-
def test_22_verify(self):
"test verify() scheme kwd"
handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"]
@@ -915,14 +911,6 @@ class CryptContextTest(TestCase):
#check verify using wrong alg
self.assertRaises(ValueError, cc.verify, 'test', h, scheme='bsdi_crypt')
- def test_23_verify_empty_hash(self):
- "test verify() allows hash=None"
- handlers = [hash.md5_crypt, hash.des_crypt, hash.bsdi_crypt]
- cc = CryptContext(handlers, policy=None)
- self.assertTrue(not cc.verify("test", None))
- for handler in handlers:
- self.assertTrue(not cc.verify("test", None, scheme=handler.name))
-
def test_24_min_verify_time(self):
"test verify() honors min_verify_time"
#NOTE: this whole test assumes time.sleep() and tick()
@@ -1008,6 +996,58 @@ class CryptContextTest(TestCase):
self.assertIs(new_hash, None)
#=========================================================
+ # border cases
+ #=========================================================
+ def test_30_nonstring_hash(self):
+ "test non-string hash values cause error"
+ #
+ # test hash=None or some other non-string causes TypeError
+ # and that explicit-scheme code path behaves the same.
+ #
+ cc = CryptContext(["des_crypt"])
+ for hash, kwds in [
+ (None, {}),
+ (None, {"scheme": "des_crypt"}),
+ (1, {}),
+ ((), {}),
+ ]:
+
+ self.assertRaises(TypeError, cc.identify, hash, **kwds)
+ self.assertRaises(TypeError, cc.genhash, 'stub', hash, **kwds)
+ self.assertRaises(TypeError, cc.verify, 'stub', hash, **kwds)
+ self.assertRaises(TypeError, cc.verify_and_update, 'stub', hash, **kwds)
+ self.assertRaises(TypeError, cc.hash_needs_update, hash, **kwds)
+
+ #
+ # but genhash *should* accept None if default scheme lacks config string.
+ #
+ cc2 = CryptContext(["mysql323"])
+ self.assertRaises(TypeError, cc2.identify, None)
+ self.assertIsInstance(cc2.genhash("stub", None), str)
+ self.assertRaises(TypeError, cc2.verify, 'stub', None)
+ self.assertRaises(TypeError, cc2.verify_and_update, 'stub', None)
+ self.assertRaises(TypeError, cc2.hash_needs_update, None)
+
+
+ def test_31_nonstring_secret(self):
+ "test non-string password values cause error"
+ cc = CryptContext(["des_crypt"])
+ hash = cc.encrypt("stub")
+ #
+ # test secret=None, or some other non-string causes TypeError
+ #
+ for secret, kwds in [
+ (None, {}),
+ (None, {"scheme": "des_crypt"}),
+ (1, {}),
+ ((), {}),
+ ]:
+ self.assertRaises(TypeError, cc.encrypt, secret, **kwds)
+ self.assertRaises(TypeError, cc.genhash, secret, hash, **kwds)
+ self.assertRaises(TypeError, cc.verify, secret, hash, **kwds)
+ self.assertRaises(TypeError, cc.verify_and_update, secret, hash, **kwds)
+
+ #=========================================================
# other
#=========================================================
def test_90_bcrypt_normhash(self):
diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py
index 30194a6..5044d1e 100644
--- a/passlib/tests/test_utils_handlers.py
+++ b/passlib/tests/test_utils_handlers.py
@@ -14,7 +14,7 @@ from passlib.hash import ldap_md5, sha256_crypt
from passlib.registry import _unload_handler_name as unload_handler_name, \
register_crypt_handler, get_crypt_handler
from passlib.exc import MissingBackendError, PasslibHashWarning
-from passlib.utils import getrandstr, JYTHON, rng, to_unicode
+from passlib.utils import getrandstr, JYTHON, rng
from passlib.utils.compat import b, bytes, bascii_to_str, str_to_uascii, \
uascii_to_str, unicode, PY_MAX_25
import passlib.utils.handlers as uh
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index b7dce87..5665259 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -42,7 +42,7 @@ from passlib.utils import has_rounds_info, has_salt_info, rounds_cost_values, \
classproperty, rng, getrandstr, is_ascii_safe, to_native_str, \
repeat_string
from passlib.utils.compat import b, bytes, iteritems, irange, callable, \
- sb_types, exc_err, u, unicode
+ base_string_types, exc_err, u, unicode
import passlib.utils.handlers as uh
#local
__all__ = [
@@ -374,7 +374,7 @@ class TestCase(unittest.TestCase):
# 3.0 and <= 2.6 didn't have this method at all
def assertRegex(self, text, expected_regex, msg=None):
"""Fail the test unless the text matches the regular expression."""
- if isinstance(expected_regex, sb_types):
+ if isinstance(expected_regex, base_string_types):
assert expected_regex, "expected_regex must not be empty."
expected_regex = re.compile(expected_regex)
if not expected_regex.search(text):
@@ -662,6 +662,10 @@ class HandlerCase(TestCase):
msg = "verify failed: secret=%r, hash=%r" % (secret, hash)
raise self.failureException(msg)
+ def check_returned_native_str(self, result, func_name):
+ self.assertIsInstance(result, str,
+ "%s() failed to return native string: %r" % (func_name, result,))
+
#=========================================================
# internal class attrs
#=========================================================
@@ -765,8 +769,7 @@ class HandlerCase(TestCase):
# encrypt should generate hash...
result = self.do_encrypt(secret)
- self.assertIsInstance(result, str,
- "encrypt must return native str:")
+ self.check_returned_native_str(result, "encrypt")
# which should be positively identifiable...
self.assertTrue(self.do_identify(result))
@@ -1202,17 +1205,22 @@ class HandlerCase(TestCase):
self.assertNotEqual(h2, h1,
"genhash() should be case sensitive")
- def test_62_secret_null(self):
- "test password=None"
- _, hash = self.get_sample_hash()
+ def test_62_secret_border(self):
+ "test non-string passwords are rejected"
+ hash = self.get_sample_hash()[1]
+
+ # secret=None
self.assertRaises(TypeError, self.do_encrypt, None)
self.assertRaises(TypeError, self.do_genhash, None, hash)
self.assertRaises(TypeError, self.do_verify, None, hash)
- def test_63_max_password_size(self):
+ # secret=int (picked as example of entirely wrong class)
+ self.assertRaises(TypeError, self.do_encrypt, 1)
+ self.assertRaises(TypeError, self.do_genhash, 1, hash)
+ self.assertRaises(TypeError, self.do_verify, 1, hash)
+
+ def test_63_large_secret(self):
"test MAX_PASSWORD_SIZE is enforced"
- if self.is_disabled_handler:
- raise self.skipTest("not applicable")
from passlib.exc import PasswordSizeError
from passlib.utils import MAX_PASSWORD_SIZE
secret = '.' * (1+MAX_PASSWORD_SIZE)
@@ -1400,25 +1408,28 @@ class HandlerCase(TestCase):
__msg__= "genhash() failed to throw error for hash "
"belonging to %s: %r" % (name, hash))
- def test_76_none(self):
- "test empty hashes"
+ def test_76_hash_border(self):
+ "test non-string hashes are rejected"
#
- # test hash=None
+ # test hash=None is rejected (except if config=None)
#
- # FIXME: allowing value or type error to simplify implementation,
- # but TypeError is really the correct one here.
- self.assertFalse(self.do_identify(None))
- self.assertRaises((ValueError, TypeError), self.do_verify, 'stub', None)
+ self.assertRaises(TypeError, self.do_identify, None)
+ self.assertRaises(TypeError, self.do_verify, 'stub', None)
if self.supports_config_string:
- self.assertRaises((ValueError, TypeError), self.do_genhash,
- 'stub', None)
+ self.assertRaises(TypeError, self.do_genhash, 'stub', None)
else:
result = self.do_genhash('stub', None)
- self.assertIsInstance(result, str,
- "genhash() failed to return native string: %r" % (result,))
+ self.check_returned_native_str(result, "genhash")
#
- # test hash=''
+ # test hash=int is rejected (picked as example of entirely wrong type)
+ #
+ self.assertRaises(TypeError, self.do_identify, 1)
+ self.assertRaises(TypeError, self.do_verify, 'stub', 1)
+ self.assertRaises(TypeError, self.do_genhash, 'stub', 1)
+
+ #
+ # test hash='' is rejected for all but the plaintext hashes
#
for hash in [u(''), b('')]:
if self.accepts_all_hashes:
@@ -1426,9 +1437,9 @@ class HandlerCase(TestCase):
self.assertTrue(self.do_identify(hash))
self.do_verify('stub', hash)
result = self.do_genhash('stub', hash)
- self.assertIsInstance(result, str,
- "genhash() failed to return native string: %r" % (result,))
+ self.check_returned_native_str(result, "genhash")
else:
+ # otherwise it should reject them
self.assertFalse(self.do_identify(hash),
"identify() incorrectly identified empty hash")
self.assertRaises(ValueError, self.do_verify, 'stub', hash,
@@ -1436,6 +1447,12 @@ class HandlerCase(TestCase):
self.assertRaises(ValueError, self.do_genhash, 'stub', hash,
__msg__="genhash() failed to reject empty hash")
+ #
+ # test identify doesn't throw decoding errors on 8-bit input
+ #
+ self.do_identify('\xe2\x82\xac\xc2\xa5$') # utf-8
+ self.do_identify('abc\x91\x00') # non-utf8
+
#---------------------------------------------------------
# fuzz testing
#---------------------------------------------------------
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index d2fbd3f..b9ee776 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -17,6 +17,7 @@ import unicodedata
from warnings import warn
#site
#pkg
+from passlib.exc import ExpectedStringError
from passlib.utils.compat import add_doc, b, bytes, join_bytes, join_byte_values, \
join_byte_elems, exc_err, irange, imap, PY3, u, \
join_unicode, unicode, byte_elem_value
@@ -554,11 +555,8 @@ def to_bytes(source, encoding="utf-8", errname="value", source_encoding=None):
return source
elif isinstance(source, unicode):
return source.encode(encoding)
- elif source is None:
- raise TypeError("no %s specified" % (errname,))
else:
- raise TypeError("%s must be unicode or bytes, not %s" % (errname,
- type(source)))
+ raise ExpectedStringError(source, errname)
def to_unicode(source, source_encoding="utf-8", errname="value"):
"""helper to normalize input to unicode.
@@ -582,11 +580,8 @@ def to_unicode(source, source_encoding="utf-8", errname="value"):
return source
elif isinstance(source, bytes):
return source.decode(source_encoding)
- elif source is None:
- raise TypeError("no %s specified" % (errname,))
else:
- raise TypeError("%s must be unicode or bytes, not %s" % (errname,
- type(source)))
+ raise ExpectedStringError(source, errname)
if PY3:
def to_native_str(source, encoding="utf-8", errname="value"):
@@ -594,22 +589,16 @@ if PY3:
return source.decode(encoding)
elif isinstance(source, unicode):
return source
- elif source is None:
- raise TypeError("no %s specified" % (errname,))
else:
- raise TypeError("%s must be unicode or bytes, not %s" %
- (errname, type(source)))
+ raise ExpectedStringError(source, errname)
else:
def to_native_str(source, encoding="utf-8", errname="value"):
if isinstance(source, bytes):
return source
elif isinstance(source, unicode):
return source.encode(encoding)
- elif source is None:
- raise TypeError("no %s specified" % (errname,))
else:
- raise TypeError("%s must be unicode or bytes, not %s" %
- (errname, type(source)))
+ raise ExpectedStringError(source, errname)
add_doc(to_native_str,
"""take in unicode or bytes, return native string.
diff --git a/passlib/utils/compat.py b/passlib/utils/compat.py
index 0715f28..7bffb15 100644
--- a/passlib/utils/compat.py
+++ b/passlib/utils/compat.py
@@ -38,10 +38,11 @@ __all__ = [
'callable',
'int_types',
'num_types',
+ 'base_string_types',
# unicode/bytes types & helpers
'u', 'b',
- 'unicode', 'bytes', 'sb_types',
+ 'unicode', 'bytes',
'uascii_to_str', 'bascii_to_str',
'str_to_uascii', 'str_to_bascii',
'join_unicode', 'join_bytes',
@@ -78,6 +79,8 @@ if PY3:
assert isinstance(s, str)
return s.encode("latin-1")
+ base_string_types = (unicode, bytes)
+
else:
unicode = builtins.unicode
bytes = str if PY_MAX_25 else builtins.bytes
@@ -90,7 +93,7 @@ else:
assert isinstance(s, str)
return s
-sb_types = (unicode, bytes)
+ base_string_types = basestring
#=============================================================================
# unicode & bytes helpers
@@ -301,13 +304,13 @@ else:
# pick default end sequence
if end is None:
end = u("\n") if want_unicode else "\n"
- elif not isinstance(end, sb_types):
+ elif not isinstance(end, base_string_types):
raise TypeError("end must be None or a string")
# pick default separator
if sep is None:
sep = u(" ") if want_unicode else " "
- elif not isinstance(sep, sb_types):
+ elif not isinstance(sep, base_string_types):
raise TypeError("sep must be None or a string")
# write to buffer
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index ea8674c..fbf7b69 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -23,7 +23,7 @@ from passlib.utils import classproperty, consteq, getrandstr, getrandbytes,\
MAX_PASSWORD_SIZE
from passlib.utils.compat import b, join_byte_values, bytes, irange, u, \
uascii_to_str, join_unicode, unicode, str_to_uascii, \
- join_unicode
+ join_unicode, base_string_types
# local
__all__ = [
# helpers for implementing MCF handlers
@@ -75,6 +75,28 @@ LC_HEX_CHARS = LOWER_HEX_CHARS
_UDOLLAR = u("$")
_UZERO = u("0")
+def validate_secret(secret):
+ "ensure secret has correct type & size"
+ if not isinstance(secret, base_string_types):
+ raise exc.ExpectedStringError(secret, "secret")
+ if len(secret) > MAX_PASSWORD_SIZE:
+ raise exc.PasswordSizeError()
+
+def to_unicode_for_identify(hash):
+ "convert hash to unicode for identify method"
+ if isinstance(hash, unicode):
+ return hash
+ elif isinstance(hash, bytes):
+ # try as utf-8, but if it fails, use foolproof latin-1,
+ # since we don't really care about non-ascii chars
+ # when running identify.
+ try:
+ return hash.decode("utf-8")
+ except UnicodeDecodeError:
+ return hash.decode("latin-1")
+ else:
+ raise exc.ExpectedStringError(hash, "hash")
+
def parse_mc2(hash, prefix, sep=_UDOLLAR, handler=None):
"""parse hash using 2-part modular crypt format.
@@ -90,10 +112,7 @@ def parse_mc2(hash, prefix, sep=_UDOLLAR, handler=None):
a ``(salt, chk | None)`` tuple.
"""
# detect prefix
- if not hash:
- raise exc.MissingHashError(handler)
- if isinstance(hash, bytes):
- hash = hash.decode('ascii')
+ hash = to_unicode(hash, "ascii", "hash")
assert isinstance(prefix, unicode)
if not hash.startswith(prefix):
raise exc.InvalidHashError(handler)
@@ -132,10 +151,7 @@ def parse_mc3(hash, prefix, sep=_UDOLLAR, rounds_base=10,
a ``(rounds : int, salt, chk | None)`` tuple.
"""
# detect prefix
- if not hash:
- raise exc.MissingHashError(handler)
- if isinstance(hash, bytes):
- hash = hash.decode('ascii')
+ hash = to_unicode(hash, "ascii", "hash")
assert isinstance(prefix, unicode)
if not hash.startswith(prefix):
raise exc.InvalidHashError(handler)
@@ -431,26 +447,18 @@ 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
-
+ hash = to_unicode_for_identify(hash)
if not hash:
return False
# does class specify a known unique prefix to look for?
ident = cls.ident
if ident is not None:
- assert isinstance(ident, unicode)
- if isinstance(hash, bytes):
- ident = ident.encode('ascii')
return hash.startswith(ident)
# 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.
@@ -513,8 +521,7 @@ class GenericHandler(object):
@classmethod
def genhash(cls, secret, config, **context):
- if secret and len(secret) > MAX_PASSWORD_SIZE:
- raise exc.PasswordSizeError()
+ validate_secret(secret)
self = cls.from_string(config, **context)
self.checksum = self._calc_checksum(secret)
return self.to_string()
@@ -522,6 +529,9 @@ class GenericHandler(object):
def _calc_checksum(self, secret): #pragma: no cover
"""given secret; calcuate and return encoded checksum portion of hash
string, taking config from object state
+
+ calc checksum implementations may assume secret is always
+ either unicode or bytes, checks are performed by verify/etc.
"""
raise NotImplementedError("%s must implement _calc_checksum()" %
(self.__class__,))
@@ -531,8 +541,7 @@ class GenericHandler(object):
#=========================================================
@classmethod
def encrypt(cls, secret, **kwds):
- if secret and len(secret) > MAX_PASSWORD_SIZE:
- raise exc.PasswordSizeError()
+ validate_secret(secret)
self = cls(use_defaults=True, **kwds)
self.checksum = self._calc_checksum(secret)
return self.to_string()
@@ -542,8 +551,7 @@ class GenericHandler(object):
# 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.
- if secret and len(secret) > MAX_PASSWORD_SIZE:
- raise exc.PasswordSizeError()
+ validate_secret(secret)
self = cls.from_string(hash, **context)
chk = self.checksum
if chk is None:
@@ -607,7 +615,7 @@ class StaticHandler(GenericHandler):
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 = to_unicode(hash, "ascii", "hash")
hash = cls._norm_hash(hash)
# could enable this for extra strictness
##pat = cls._hash_regex
@@ -792,22 +800,13 @@ class HasManyIdents(GenericHandler):
#=========================================================
@classmethod
def identify(cls, hash):
- if not hash:
- return False
- if isinstance(hash, bytes):
- try:
- hash = hash.decode('ascii')
- except UnicodeDecodeError:
- return False
+ hash = to_unicode_for_identify(hash)
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 exc.MissingHashError(cls)
- if isinstance(hash, bytes):
- hash = hash.decode("ascii")
+ hash = to_unicode(hash, "ascii", "hash")
for ident in cls.ident_values:
if hash.startswith(ident):
return ident, hash[len(ident):]
@@ -1484,8 +1483,7 @@ class PrefixWrapper(object):
def _unwrap_hash(self, hash):
"given hash belonging to wrapper, return orig version"
- if isinstance(hash, bytes):
- hash = hash.decode('ascii')
+ # NOTE: assumes hash has been validated as unicode already
prefix = self.prefix
if not hash.startswith(prefix):
raise exc.InvalidHashError(self)
@@ -1494,10 +1492,10 @@ class PrefixWrapper(object):
def _wrap_hash(self, hash):
"given orig hash; return one belonging to wrapper"
- #NOTE: should usually be native string.
+ # NOTE: should usually be native string.
# (which does mean extra work under py2, but not py3)
if isinstance(hash, bytes):
- hash = hash.decode('ascii')
+ hash = hash.decode("ascii")
orig_prefix = self.orig_prefix
if not hash.startswith(orig_prefix):
raise exc.InvalidHashError(self.wrapped)
@@ -1505,10 +1503,7 @@ class PrefixWrapper(object):
return uascii_to_str(wrapped)
def identify(self, hash):
- if not hash:
- return False
- if isinstance(hash, bytes):
- hash = hash.decode('ascii')
+ hash = to_unicode_for_identify(hash)
if not hash.startswith(self.prefix):
return False
hash = self._unwrap_hash(hash)
@@ -1516,13 +1511,14 @@ class PrefixWrapper(object):
def genconfig(self, **kwds):
config = self.wrapped.genconfig(**kwds)
- if config:
- return self._wrap_hash(config)
+ if config is None:
+ return None
else:
- return config
+ return self._wrap_hash(config)
def genhash(self, secret, config, **kwds):
- if config:
+ if config is not None:
+ config = to_unicode(config, "ascii", "config/hash")
config = self._unwrap_hash(config)
return self._wrap_hash(self.wrapped.genhash(secret, config, **kwds))
@@ -1530,8 +1526,7 @@ class PrefixWrapper(object):
return self._wrap_hash(self.wrapped.encrypt(secret, **kwds))
def verify(self, secret, hash, **kwds):
- if not hash:
- raise exc.MissingHashError(self)
+ hash = to_unicode(hash, "ascii", "hash")
hash = self._unwrap_hash(hash)
return self.wrapped.verify(secret, hash, **kwds)
diff --git a/passlib/utils/pbkdf2.py b/passlib/utils/pbkdf2.py
index 086865b..a9b7636 100644
--- a/passlib/utils/pbkdf2.py
+++ b/passlib/utils/pbkdf2.py
@@ -21,7 +21,7 @@ except ImportError:
_EVP = None
#pkg
from passlib.exc import PasslibRuntimeWarning
-from passlib.utils import to_bytes, xor_bytes, to_native_str
+from passlib.utils import xor_bytes, to_native_str
from passlib.utils.compat import b, bytes, BytesIO, irange, callable, int_types
#local
__all__ = [