diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2012-04-11 17:49:09 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2012-04-11 17:49:09 -0400 |
| commit | 5bd6deb8144cb24caa51e82c7682f706ecc09a6c (patch) | |
| tree | 0eca5ec7a8a145cb3e166a9a75b95b393e9d417d /passlib | |
| parent | 157d4806512b2586c1a0fd5ee57e8c167e506f3e (diff) | |
| download | passlib-5bd6deb8144cb24caa51e82c7682f706ecc09a6c.tar.gz | |
clarify behavior for secret=None and hash=None
* passing a non-string secret or non-string hash to any
CryptContext or handler method will now reliably result
in a TypeError.
previously, passing hash=None to many handler identify() and verify()
methods would return False, while others would raise a TypeError.
other handler methods would alternately throw ValueError or TypeError
when passed a value that wasn't unicode or bytes.
the various CryptContext methods also behaved inconsistently,
depending on the behavior of the underlying handler.
all of these behaviors are gone, they should all raise the same TypeError.
* redid many of the from_string() methods to verify the hash type.
* moved secret type & size validation to GenericHandler's encrypt/genhash/verify methods.
this cheaply made the secret validation global to all hashes, and lets
_calc_digest() implementations trust that the secret is valid.
* updated the CryptContext and handler unittests to verify the above behavior is adhered to.
Diffstat (limited to 'passlib')
27 files changed, 278 insertions, 294 deletions
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__ = [ |
