def test_to_native_str(self): "test to_native_str()" from passlib.utils import to_native_str # test plain ascii self.assertEqual(to_native_str(u('abc'), 'ascii'), 'abc') self.assertEqual(to_native_str(b('abc'), 'ascii'), 'abc') # test invalid ascii if PY3: self.assertEqual(to_native_str(u('\xE0'), 'ascii'), '\xE0') self.assertRaises(UnicodeDecodeError, to_native_str, b('\xC3\xA0'), 'ascii') else: self.assertRaises(UnicodeEncodeError, to_native_str, u('\xE0'), 'ascii') self.assertEqual(to_native_str(b('\xC3\xA0'), 'ascii'), '\xC3\xA0') # test latin-1 self.assertEqual(to_native_str(u('\xE0'), 'latin-1'), '\xE0') self.assertEqual(to_native_str(b('\xE0'), 'latin-1'), '\xE0') # test utf-8 self.assertEqual(to_native_str(u('\xE0'), 'utf-8'), '\xE0' if PY3 else '\xC3\xA0') self.assertEqual(to_native_str(b('\xC3\xA0'), 'utf-8'), '\xE0' if PY3 else '\xC3\xA0') # other types rejected self.assertRaises(TypeError, to_native_str, None, 'ascii')
def genhash(cls, secret, config, marker=None): uh.validate_secret(secret) if config is not None and not cls.identify(config): # handles typecheck raise uh.exc.InvalidHashError(cls) if config: # we want to preserve the existing str, # since it might contain a disabled password hash ("!" + hash) return to_native_str(config, param="config") # if None or empty string, replace with marker if marker: if not cls.identify(marker): raise ValueError("invalid marker: %r" % marker) else: marker = cls.default_marker assert marker and cls.identify(marker) return to_native_str(marker, param="marker")
def verify(cls, secret, hash, encoding=None): if not encoding: encoding = cls.default_encoding hash = to_native_str(hash, encoding, "hash") if not cls.identify(hash): raise uh.exc.InvalidHashError(cls) return consteq(cls.encrypt(secret, encoding), hash)
def hash(cls, secret, **kwds): if kwds: uh.warn_hash_settings_deprecation(cls, kwds) return cls.using(**kwds).hash(secret) uh.validate_secret(secret) marker = cls.default_marker assert marker and cls.identify(marker) return to_native_str(marker, param="marker")
def _norm_hash(cls, hash): "normalize hash to native string, and validate it" hash = to_native_str(hash, param="hash") if len(hash) != 32: raise uh.exc.MalformedHashError(cls, "wrong size") for char in hash: if char not in uh.LC_HEX_CHARS: raise uh.exc.MalformedHashError(cls, "invalid chars in hash") return hash
def _norm_hash(cls, hash): """normalize hash to native string, and validate it""" hash = to_native_str(hash, param="hash") if len(hash) != 32: raise uh.exc.MalformedHashError(cls, "wrong size") for char in hash: if char not in uh.LC_HEX_CHARS: raise uh.exc.MalformedHashError(cls, "invalid chars in hash") return hash
def check_pybcrypt(secret, hash): "pybcrypt" secret = to_native_str(secret, self.fuzz_password_encoding) if hash.startswith(IDENT_2Y): hash = IDENT_2A + hash[4:] try: return bcrypt.hashpw(secret, hash) == hash except ValueError: raise ValueError("py-bcrypt rejected hash: %r" % (hash,))
def check_pybcrypt(secret, hash): "pybcrypt" secret = to_native_str(secret, self.fuzz_password_encoding) if hash.startswith(IDENT_2Y): hash = IDENT_2A + hash[4:] try: return bcrypt.hashpw(secret, hash) == hash except ValueError: raise ValueError("py-bcrypt rejected hash: %r" % (hash, ))
def disable(cls, hash=None): out = cls.hash("") if hash is not None: hash = to_native_str(hash, param="hash") if cls.identify(hash): # extract original hash, so that we normalize marker hash = cls.enable(hash) if hash: out += hash return out
def enable(cls, hash): hash = to_native_str(hash, param="hash") for prefix in cls._disable_prefixes: if hash.startswith(prefix): orig = hash[len(prefix):] if orig: return orig else: raise ValueError("cannot restore original hash") raise uh.exc.InvalidHashError(cls)
def check_pybcrypt(secret, hash): """pybcrypt""" secret = to_native_str(secret, self.fuzz_password_encoding) if len(secret) > 200: # vulnerable to wraparound bug secret = secret[:200] if hash.startswith((IDENT_2B, IDENT_2Y)): hash = IDENT_2A + hash[4:] try: return bcrypt.hashpw(secret, hash) == hash except ValueError: raise ValueError("py-bcrypt rejected hash: %r" % (hash,))
def genhash(cls, secret, config, marker=None): if not cls.identify(config): raise uh.exc.InvalidHashError(cls) elif config: # preserve the existing str,since it might contain a disabled password hash ("!" + hash) uh.validate_secret(secret) return to_native_str(config, param="config") else: if marker is not None: cls = cls.using(marker=marker) return cls.hash(secret)
def check_bcryptor(secret, hash): "bcryptor" secret = to_native_str(secret, self.fuzz_password_encoding) if hash.startswith(IDENT_2Y): hash = IDENT_2A + hash[4:] elif hash.startswith(IDENT_2): # bcryptor doesn't support $2$ hashes; but we can fake it # using the $2a$ algorithm, by repeating the password until # it's 72 chars in length. hash = IDENT_2A + hash[3:] if secret: secret = repeat_string(secret, 72) return Engine(False).hash_key(secret, hash) == hash
def check_bcryptor(secret, hash): """bcryptor""" secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding) if hash.startswith((IDENT_2B, IDENT_2Y)): hash = IDENT_2A + hash[4:] elif hash.startswith(IDENT_2): # bcryptor doesn't support $2$ hashes; but we can fake it # using the $2a$ algorithm, by repeating the password until # it's 72 chars in length. hash = IDENT_2A + hash[3:] if secret: secret = repeat_string(secret, 72) return Engine(False).hash_key(secret, hash) == hash
def check_pybcrypt(secret, hash): """pybcrypt""" secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding) if len(secret) > 200: # vulnerable to wraparound bug secret = secret[:200] if hash.startswith((IDENT_2B, IDENT_2Y)): hash = IDENT_2A + hash[4:] try: if lock: with lock: return bcrypt_mod.hashpw(secret, hash) == hash else: return bcrypt_mod.hashpw(secret, hash) == hash except ValueError: raise ValueError("py-bcrypt rejected hash: %r" % (hash, ))
def from_string(cls, hash): hash = to_native_str(hash, "ascii", "hash") if not hash.startswith("$scram$"): raise uh.exc.InvalidHashError(cls) parts = hash[7:].split("$") if len(parts) != 3: raise uh.exc.MalformedHashError(cls) rounds_str, salt_str, chk_str = parts # decode rounds rounds = int(rounds_str) if rounds_str != str(rounds): # forbid zero padding, etc. raise uh.exc.MalformedHashError(cls) # decode salt try: salt = ab64_decode(salt_str.encode("ascii")) except TypeError: raise uh.exc.MalformedHashError(cls) # decode algs/digest list if not chk_str: # scram hashes MUST have something here. raise uh.exc.MalformedHashError(cls) elif "=" in chk_str: # comma-separated list of 'alg=digest' pairs algs = None chkmap = {} for pair in chk_str.split(","): alg, digest = pair.split("=") try: chkmap[alg] = ab64_decode(digest.encode("ascii")) except TypeError: raise uh.exc.MalformedHashError(cls) else: # comma-separated list of alg names, no digests algs = chk_str chkmap = None # return new object return cls( rounds=rounds, salt=salt, checksum=chkmap, algs=algs, )
def test_to_native_str(self): "test to_native_str()" self.assertEqual(to_native_str(u'abc'), 'abc') self.assertEqual(to_native_str(b('abc')), 'abc') self.assertRaises(TypeError, to_native_str, None) # Py2k # self.assertEqual(to_native_str(u'\x00\xff'), b('\x00\xc3\xbf')) self.assertEqual(to_native_str(b('\x00\xc3\xbf')), b('\x00\xc3\xbf')) self.assertEqual(to_native_str(u'\x00\xff', 'latin-1'), b('\x00\xff')) self.assertEqual(to_native_str(b('\x00\xff'), 'latin-1'), b('\x00\xff'))
def test_md4_digest(self): "test md4 digest()" md4 = self.hash for input, hex in self.vectors: out = md4(input).digest() self.assertEqual(to_native_str(hexlify(out)), hex)
def _get_hash_aliases(name): """ internal helper used by :func:`lookup_hash` -- normalize arbitrary hash name to hashlib format. if name not recognized, returns dummy record and issues a warning. :arg name: unnormalized name :returns: tuple with 2+ elements: ``(hashlib_name, iana_name|None, ... 0+ aliases)``. """ # normalize input orig = name if not isinstance(name, str): name = to_native_str(name, 'utf-8', 'hash name') name = re.sub("[_ /]", "-", name.strip().lower()) if name.startswith("scram-"): # helper for SCRAM protocol (see passlib.handlers.scram) name = name[6:] if name.endswith("-plus"): name = name[:-5] # look through standard names and known aliases def check_table(name): for row in _known_hash_names: if name in row: return row result = check_table(name) if result: return result # try to clean name up some more m = re.match(r"(?i)^(?P<name>[a-z]+)-?(?P<rev>\d)?-?(?P<size>\d{3,4})?$", name) if m: # roughly follows "SHA2-256" style format, normalize representation, # and checked table. iana_name, rev, size = m.group("name", "rev", "size") if rev: iana_name += rev hashlib_name = iana_name if size: iana_name += "-" + size if rev: hashlib_name += "_" hashlib_name += size result = check_table(iana_name) if result: return result # not found in table, but roughly recognize format. use names we built up as fallback. log.info("normalizing unrecognized hash name %r => %r / %r", orig, hashlib_name, iana_name) else: # just can't make sense of it. return something iana_name = name hashlib_name = name.replace("-", "_") log.warning("normalizing unrecognized hash name and format %r => %r / %r", orig, hashlib_name, iana_name) return hashlib_name, iana_name
def encrypt(cls, secret, encoding=None): uh.validate_secret(secret) if not encoding: encoding = cls.default_encoding return to_native_str(secret, encoding, "secret")
def hexdigest(self): return to_native_str(hexlify(self.digest()), "latin-1")
def _get_hash_aliases(name): """ internal helper used by :func:`lookup_hash` -- normalize arbitrary hash name to hashlib format. if name not recognized, returns dummy record and issues a warning. :arg name: unnormalized name :returns: tuple with 2+ elements: ``(hashlib_name, iana_name|None, ... 0+ aliases)``. """ # normalize input orig = name if not isinstance(name, str): name = to_native_str(name, 'utf-8', 'hash name') name = re.sub("[_ /]", "-", name.strip().lower()) if name.startswith( "scram-" ): # helper for SCRAM protocol (see passlib.handlers.scram) name = name[6:] if name.endswith("-plus"): name = name[:-5] # look through standard names and known aliases def check_table(name): for row in _known_hash_names: if name in row: return row result = check_table(name) if result: return result # try to clean name up some more m = re.match(r"(?i)^(?P<name>[a-z]+)-?(?P<rev>\d)?-?(?P<size>\d{3,4})?$", name) if m: # roughly follows "SHA2-256" style format, normalize representation, # and checked table. iana_name, rev, size = m.group("name", "rev", "size") if rev: iana_name += rev hashlib_name = iana_name if size: iana_name += "-" + size if rev: hashlib_name += "_" hashlib_name += size result = check_table(iana_name) if result: return result # not found in table, but roughly recognize format. use names we built up as fallback. log.info("normalizing unrecognized hash name %r => %r / %r", orig, hashlib_name, iana_name) else: # just can't make sense of it. return something iana_name = name hashlib_name = name.replace("-", "_") log.warning( "normalizing unrecognized hash name and format %r => %r / %r", orig, hashlib_name, iana_name) return hashlib_name, iana_name
def norm_hash_name(name, format="hashlib"): """Normalize hash function name :arg name: Original hash function name. This name can be a Python :mod:`~hashlib` digest name, a SCRAM mechanism name, IANA assigned hash name, etc. Case is ignored, and underscores are converted to hyphens. :param format: Naming convention to normalize to. Possible values are: * ``"hashlib"`` (the default) - normalizes name to be compatible with Python's :mod:`!hashlib`. * ``"iana"`` - normalizes name to IANA-assigned hash function name. for hashes which IANA hasn't assigned a name for, issues a warning, and then uses a heuristic to give a "best guess". :returns: Hash name, returned as native :class:`!str`. """ # check cache try: idx = _nhn_formats[format] except KeyError: raise ValueError("unknown format: %r" % (format,)) try: return _nhn_cache[name][idx] except KeyError: pass orig = name # normalize input if not isinstance(name, str): name = to_native_str(name, 'utf-8', 'hash name') name = re.sub("[_ /]", "-", name.strip().lower()) if name.startswith("scram-"): name = name[6:] if name.endswith("-plus"): name = name[:-5] # look through standard names and known aliases def check_table(name): for row in _nhn_hash_names: if name in row: _nhn_cache[orig] = row return row[idx] result = check_table(name) if result: return result # try to clean name up, and recheck table m = re.match("^(?P<name>[a-z]+)-?(?P<rev>\d)?-?(?P<size>\d{3,4})?$", name) if m: name, rev, size = m.group("name", "rev", "size") if rev: name += rev if size: name += "-" + size result = check_table(name) if result: return result # else we've done what we can warn("norm_hash_name(): unknown hash: %r" % (orig,), PasslibRuntimeWarning) name2 = name.replace("-", "") row = _nhn_cache[orig] = (name2, name) return row[idx]