diff options
30 files changed, 298 insertions, 305 deletions
@@ -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__ = [ |
