def test_register_crypt_handler(self): """test register_crypt_handler()""" self.assertRaises(TypeError, register_crypt_handler, {}) self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name=None))) self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="AB_CD"))) self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab-cd"))) self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab__cd"))) self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="default"))) class dummy_1(uh.StaticHandler): name = "dummy_1" class dummy_1b(uh.StaticHandler): name = "dummy_1" self.assertTrue('dummy_1' not in list_crypt_handlers()) register_crypt_handler(dummy_1) register_crypt_handler(dummy_1) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) self.assertRaises(KeyError, register_crypt_handler, dummy_1b) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) register_crypt_handler(dummy_1b, force=True) self.assertIs(get_crypt_handler("dummy_1"), dummy_1b) self.assertTrue('dummy_1' in list_crypt_handlers())
def test_get_crypt_handler(self): """test get_crypt_handler()""" class dummy_1(uh.StaticHandler): name = "dummy_1" # without available handler self.assertRaises(KeyError, get_crypt_handler, "dummy_1") self.assertIs(get_crypt_handler("dummy_1", None), None) # already loaded handler register_crypt_handler(dummy_1) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) with warnings.catch_warnings(): warnings.filterwarnings("ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning) # already loaded handler, using incorrect name self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1) # lazy load of unloaded handler, using incorrect name register_crypt_handler_path('dummy_0', __name__) self.assertIs(get_crypt_handler("DUMMY-0"), dummy_0) # check system & private names aren't returned import passlib.hash # ensure module imported, so py3.3 sets __package__ passlib.hash.__dict__["_fake"] = "dummy" # so behavior seen under py2x also for name in ["_fake", "__package__"]: self.assertRaises(KeyError, get_crypt_handler, name) self.assertIs(get_crypt_handler(name, None), None)
def test_get_crypt_handler(self): """test get_crypt_handler()""" class dummy_1(uh.StaticHandler): name = "dummy_1" # without available handler self.assertRaises(KeyError, get_crypt_handler, "dummy_1") self.assertIs(get_crypt_handler("dummy_1", None), None) # already loaded handler register_crypt_handler(dummy_1) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) with warnings.catch_warnings(): warnings.filterwarnings( "ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning) # already loaded handler, using incorrect name self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1) # lazy load of unloaded handler, using incorrect name register_crypt_handler_path('dummy_0', __name__) self.assertIs(get_crypt_handler("DUMMY-0"), dummy_0) # check system & private names aren't returned from passlib import hash hash.__dict__["_fake"] = "dummy" for name in ["_fake", "__package__"]: self.assertRaises(KeyError, get_crypt_handler, name) self.assertIs(get_crypt_handler(name, None), None)
def test_get_crypt_handler(self): "test get_crypt_handler()" class dummy_1(uh.StaticHandler): name = "dummy_1" self.assertRaises(KeyError, get_crypt_handler, "dummy_1") register_crypt_handler(dummy_1) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) with catch_warnings(): warnings.filterwarnings("ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning) self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1)
def get_passlib_hasher(handler, algorithm=None): """create *Hasher*-compatible wrapper for specified passlib hash. This takes in the name of a passlib hash (or the handler object itself), and returns a wrapper instance which should be compatible with Django 1.4's Hashers framework. If the named hash corresponds to one of Django's builtin hashers, an instance of the real hasher class will be returned. Note that the format of the handler won't be altered, so will probably not be compatible with Django's algorithm format, so the monkeypatch provided by this plugin must have been applied. """ if isinstance(handler, str): handler = get_crypt_handler(handler) if hasattr(handler, "django_name"): # return native hasher instance # XXX: should add this to _hasher_cache[] name = handler.django_name if name == "sha1" and algorithm == "unsalted_sha1": # django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes, # but passlib just reuses the "sha1$salt$digest" handler. # we want to resolve to correct django hasher. name = algorithm return _get_hasher(name) if handler.name == "django_disabled": raise ValueError("can't wrap unusable-password handler") try: return _hasher_cache[handler] except KeyError: name = "Passlib_%s_PasswordHasher" % handler.name.title() cls = type(name, (_HasherWrapper, ), dict(passlib_handler=handler)) hasher = _hasher_cache[handler] = cls() return hasher
def get_passlib_hasher(handler, algorithm=None): """create *Hasher*-compatible wrapper for specified passlib hash. This takes in the name of a passlib hash (or the handler object itself), and returns a wrapper instance which should be compatible with Django 1.4's Hashers framework. If the named hash corresponds to one of Django's builtin hashers, an instance of the real hasher class will be returned. Note that the format of the handler won't be altered, so will probably not be compatible with Django's algorithm format, so the monkeypatch provided by this plugin must have been applied. """ if isinstance(handler, str): handler = get_crypt_handler(handler) if hasattr(handler, "django_name"): # return native hasher instance # XXX: should add this to _hasher_cache[] name = handler.django_name if name == "sha1" and algorithm == "unsalted_sha1": # django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes, # but passlib just reuses the "sha1$salt$digest" handler. # we want to resolve to correct django hasher. name = algorithm return _get_hasher(name) if handler.name == "django_disabled": raise ValueError("can't wrap unusable-password handler") try: return _hasher_cache[handler] except KeyError: name = "Passlib_%s_PasswordHasher" % handler.name.title() cls = type(name, (_HasherWrapper,), dict(passlib_handler=handler)) hasher = _hasher_cache[handler] = cls() return hasher
def get_passlib_hasher(handler): """create *Hasher*-compatible wrapper for specified passlib hash. This takes in the name of a passlib hash (or the handler object itself), and returns a wrapper instance which should be compatible with Django 1.4's Hashers framework. If the named hash corresponds to one of Django's builtin hashers, an instance of the real hasher class will be returned. Note that the format of the handler won't be altered, so will probably not be compatible with Django's algorithm format, so the monkeypatch provided by this plugin must have been applied. .. note:: This function requires Django 1.4 or later. """ if DJANGO_VERSION < (1,4): raise RuntimeError("get_passlib_hasher() requires Django >= 1.4") if isinstance(handler, str): handler = get_crypt_handler(handler) if hasattr(handler, "django_name"): # return native hasher instance # XXX: should cache this too. return _get_hasher(handler.django_name) if handler.name == "django_disabled": raise ValueError("can't wrap unusable-password handler") try: return _hasher_cache[handler] except KeyError: name = "Passlib_%s_PasswordHasher" % handler.name.title() cls = type(name, (_HasherWrapper,), dict(passlib_handler=handler)) hasher = _hasher_cache[handler] = cls() return hasher
def test_hash_proxy(self): """test passlib.hash proxy object""" # check dir works dir(hash) # check repr works repr(hash) # check non-existent attrs raise error self.assertRaises(AttributeError, getattr, hash, 'fooey') # GAE tries to set __loader__, # make sure that doesn't call register_crypt_handler. old = getattr(hash, "__loader__", None) test = object() hash.__loader__ = test self.assertIs(hash.__loader__, test) if old is None: del hash.__loader__ self.assertFalse(hasattr(hash, "__loader__")) else: hash.__loader__ = old self.assertIs(hash.__loader__, old) # check storing attr calls register_crypt_handler class dummy_1(uh.StaticHandler): name = "dummy_1" hash.dummy_1 = dummy_1 self.assertIs(get_crypt_handler("dummy_1"), dummy_1) # check storing under wrong name results in error self.assertRaises(ValueError, setattr, hash, "dummy_1x", dummy_1)
def get_passlib_hasher(handler): """create *Hasher*-compatible wrapper for specified passlib hash. This takes in the name of a passlib hash (or the handler object itself), and returns a wrapper instance which should be compatible with Django 1.4's Hashers framework. If the named hash corresponds to one of Django's builtin hashers, an instance of the real hasher class will be returned. Note that the format of the handler won't be altered, so will probably not be compatible with Django's algorithm format, so the monkeypatch provided by this plugin must have been applied. .. note:: This function requires Django 1.4 or later. """ if DJANGO_VERSION < (1, 4): raise RuntimeError("get_passlib_hasher() requires Django >= 1.4") if isinstance(handler, str): handler = get_crypt_handler(handler) if hasattr(handler, "django_name"): # return native hasher instance # XXX: should cache this too. return _get_hasher(handler.django_name) if handler.name == "django_disabled": raise ValueError("can't wrap unusable-password handler") try: return _hasher_cache[handler] except KeyError: name = "Passlib_%s_PasswordHasher" % handler.name.title() cls = type(name, (_HasherWrapper, ), dict(passlib_handler=handler)) hasher = _hasher_cache[handler] = cls() return hasher
def _get_wrapped(self): handler = self._wrapped_handler if handler is None: handler = get_crypt_handler(self._wrapped_name) self._check_handler(handler) self._wrapped_handler = handler return handler
def test_register_crypt_handler_path(self): """test register_crypt_handler_path()""" # NOTE: this messes w/ internals of registry, shouldn't be used publically. paths = registry._locations # check namespace is clear self.assertTrue("dummy_0" not in paths) self.assertFalse(hasattr(hash, "dummy_0")) # check invalid names are rejected self.assertRaises(ValueError, register_crypt_handler_path, "dummy_0", ".test_registry") self.assertRaises( ValueError, register_crypt_handler_path, "dummy_0", __name__ + ":dummy_0:xxx", ) self.assertRaises( ValueError, register_crypt_handler_path, "dummy_0", __name__ + ":dummy_0.xxx", ) # try lazy load register_crypt_handler_path("dummy_0", __name__) self.assertTrue("dummy_0" in list_crypt_handlers()) self.assertTrue("dummy_0" not in list_crypt_handlers(loaded_only=True)) self.assertIs(hash.dummy_0, dummy_0) self.assertTrue("dummy_0" in list_crypt_handlers(loaded_only=True)) unload_handler_name("dummy_0") # try lazy load w/ alt register_crypt_handler_path("dummy_0", __name__ + ":alt_dummy_0") self.assertIs(hash.dummy_0, alt_dummy_0) unload_handler_name("dummy_0") # check lazy load w/ wrong type fails register_crypt_handler_path("dummy_x", __name__) self.assertRaises(TypeError, get_crypt_handler, "dummy_x") # check lazy load w/ wrong name fails register_crypt_handler_path("alt_dummy_0", __name__) self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0") unload_handler_name("alt_dummy_0") # TODO: check lazy load which calls register_crypt_handler (warning should be issued) sys.modules.pop("passlib.tests._test_bad_register", None) register_crypt_handler_path("dummy_bad", "passlib.tests._test_bad_register") with warnings.catch_warnings(): warnings.filterwarnings("ignore", "xxxxxxxxxx", DeprecationWarning) h = get_crypt_handler("dummy_bad") from passlib.tests import _test_bad_register as tbr self.assertIs(h, tbr.alt_dummy_bad)
def _get_passlib_hasher(self, passlib_name): """ resolve passlib hasher by name, using context if available. """ context = self.context if context is None: return registry.get_crypt_handler(passlib_name) else: return context.handler(passlib_name)
def hasher_to_passlib_name(hasher_name): "convert hasher name -> passlib handler name" if hasher_name.startswith(PASSLIB_HASHER_PREFIX): return hasher_name[len(PASSLIB_HASHER_PREFIX):] for name in list_crypt_handlers(): if name.startswith(DJANGO_PASSLIB_PREFIX) or name in _other_django_hashes: handler = get_crypt_handler(name) if getattr(handler, "django_name", None) == hasher_name: return name # XXX: this should only happen for custom hashers that have been registered. # work in progress (below) that would take care of those. raise ValueError("can't translate hasher name to passlib name: %r" % hasher_name)
def hasher_to_passlib_name(hasher_name): "convert hasher name -> passlib handler name" if hasher_name.startswith(PASSLIB_HASHER_PREFIX): return hasher_name[len(PASSLIB_HASHER_PREFIX):] for name in list_crypt_handlers(): if name.startswith( DJANGO_PASSLIB_PREFIX) or name in _other_django_hashes: handler = get_crypt_handler(name) if getattr(handler, "django_name", None) == hasher_name: return name # XXX: this should only happen for custom hashers that have been registered. # _HasherHandler (below) is work in progress that would fix this. raise ValueError("can't translate hasher name to passlib name: %r" % hasher_name)
def test_get_crypt_handler(self): "test get_crypt_handler()" class dummy_1(uh.StaticHandler): name = "dummy_1" # without available handler self.assertRaises(KeyError, get_crypt_handler, "dummy_1") self.assertIs(get_crypt_handler("dummy_1", None), None) # already loaded handler register_crypt_handler(dummy_1) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) with catch_warnings(): warnings.filterwarnings("ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning) # already loaded handler, using incorrect name self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1) # lazy load of unloaded handler, using incorrect name register_crypt_handler_path('dummy_0', __name__) self.assertIs(get_crypt_handler("DUMMY-0"), dummy_0)
def check_hashes(self, tests, new_hash=None, deprecated=None): u = FakeUser() deprecated = None # check new hash construction if new_hash: u.set_password("placeholder") handler = get_crypt_handler(new_hash) self.assertTrue(handler.identify(u.password)) # run against hashes from tests... for test in tests: for secret, hash in test.all_correct_hashes: # check against valid password u.password = hash if has_django0 and isinstance(secret, unicode): secret = secret.encode("utf-8") self.assertTrue(u.check_password(secret)) if new_hash and deprecated and test.handler.name in deprecated: self.assertFalse(handler.identify(hash)) self.assertTrue(handler.identify(u.password)) # check against invalid password u.password = hash self.assertFalse(u.check_password('x'+secret)) if new_hash and deprecated and test.handler.name in deprecated: self.assertFalse(handler.identify(hash)) self.assertEquals(u.password, hash) # check disabled handling if has_django1: u.set_password(None) handler = get_crypt_handler("django_disabled") self.assertTrue(handler.identify(u.password)) self.assertFalse(u.check_password('placeholder'))
def check_hashes(self, tests, new_hash=None, deprecated=None): u = FakeUser() deprecated = None # check new hash construction if new_hash: u.set_password("placeholder") handler = get_crypt_handler(new_hash) self.assertTrue(handler.identify(u.password)) # run against hashes from tests... for test in tests: for secret, hash in test.all_correct_hashes: # check against valid password u.password = hash if has_django0 and isinstance(secret, unicode): secret = secret.encode("utf-8") self.assertTrue(u.check_password(secret)) if new_hash and deprecated and test.handler.name in deprecated: self.assertFalse(handler.identify(hash)) self.assertTrue(handler.identify(u.password)) # check against invalid password u.password = hash self.assertFalse(u.check_password('x' + secret)) if new_hash and deprecated and test.handler.name in deprecated: self.assertFalse(handler.identify(hash)) self.assertEquals(u.password, hash) # check disabled handling if has_django1: u.set_password(None) handler = get_crypt_handler("django_disabled") self.assertTrue(handler.identify(u.password)) self.assertFalse(u.check_password('placeholder'))
def _iter_os_crypt_schemes(): "helper which iterates over supported os_crypt schemes" found = False for name in unix_crypt_schemes: handler = get_crypt_handler(name) if handler.has_backend("os_crypt"): found = True yield name if found: #only offer fallback if there's another scheme in front, #as this can't actually hash any passwords yield "unix_fallback" else: #no idea what OS this could happen on, but just in case... warn("crypt.crypt() function is present, but doesn't support any formats known to passlib!")
def _iter_os_crypt_schemes(): "helper which iterates over supported os_crypt schemes" found = False for name in unix_crypt_schemes: handler = get_crypt_handler(name) if handler.has_backend("os_crypt"): found = True yield name if found: # only offer disabled handler if there's another scheme in front, # as this can't actually hash any passwords yield "unix_disabled" else: # pragma: no cover -- sanity check # no idea what OS this could happen on... warn("crypt.crypt() function is present, but doesn't support any " "formats known to passlib!", PasslibRuntimeWarning)
def _iter_os_crypt_schemes(): """helper which iterates over supported os_crypt schemes""" found = False for name in unix_crypt_schemes: handler = get_crypt_handler(name) if handler.has_backend("os_crypt"): found = True yield name if found: # only offer disabled handler if there's another scheme in front, # as this can't actually hash any passwords yield "unix_disabled" else: # pragma: no cover -- sanity check # no idea what OS this could happen on... warn("crypt.crypt() function is present, but doesn't support any " "formats known to passlib!", PasslibRuntimeWarning)
def test_register_crypt_handler_path(self): """test register_crypt_handler_path()""" # NOTE: this messes w/ internals of registry, shouldn't be used publically. paths = registry._locations # check namespace is clear self.assertTrue('dummy_0' not in paths) self.assertFalse(hasattr(hash, 'dummy_0')) # check invalid names are rejected self.assertRaises(ValueError, register_crypt_handler_path, "dummy_0", ".test_registry") self.assertRaises(ValueError, register_crypt_handler_path, "dummy_0", __name__ + ":dummy_0:xxx") self.assertRaises(ValueError, register_crypt_handler_path, "dummy_0", __name__ + ":dummy_0.xxx") # try lazy load register_crypt_handler_path('dummy_0', __name__) self.assertTrue('dummy_0' in list_crypt_handlers()) self.assertTrue('dummy_0' not in list_crypt_handlers(loaded_only=True)) self.assertIs(hash.dummy_0, dummy_0) self.assertTrue('dummy_0' in list_crypt_handlers(loaded_only=True)) unload_handler_name('dummy_0') # try lazy load w/ alt register_crypt_handler_path('dummy_0', __name__ + ':alt_dummy_0') self.assertIs(hash.dummy_0, alt_dummy_0) unload_handler_name('dummy_0') # check lazy load w/ wrong type fails register_crypt_handler_path('dummy_x', __name__) self.assertRaises(TypeError, get_crypt_handler, 'dummy_x') # check lazy load w/ wrong name fails register_crypt_handler_path('alt_dummy_0', __name__) self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0") unload_handler_name("alt_dummy_0") # TODO: check lazy load which calls register_crypt_handler (warning should be issued) sys.modules.pop("passlib.tests._test_bad_register", None) register_crypt_handler_path("dummy_bad", "passlib.tests._test_bad_register") with warnings.catch_warnings(): warnings.filterwarnings("ignore", "xxxxxxxxxx", DeprecationWarning) h = get_crypt_handler("dummy_bad") from passlib.tests import _test_bad_register as tbr self.assertIs(h, tbr.alt_dummy_bad)
def _iter_os_crypt_schemes(): "helper which iterates over supported os_crypt schemes" found = False for name in unix_crypt_schemes: handler = get_crypt_handler(name) if handler.has_backend("os_crypt"): found = True yield name if found: #only offer fallback if there's another scheme in front, #as this can't actually hash any passwords yield "unix_fallback" else: #no idea what OS this could happen on, but just in case... warn( "crypt.crypt() function is present, but doesn't support any formats known to passlib!" )
def hasher_to_passlib_name(hasher_name): """convert hasher name -> passlib handler name""" if hasher_name.startswith(PASSLIB_HASHER_PREFIX): return hasher_name[len(PASSLIB_HASHER_PREFIX):] if hasher_name == "unsalted_sha1": # django 1.4.6+ uses a separate hasher for "sha1$$digest" hashes, # but passlib just reuses the "sha1$salt$digest" handler. hasher_name = "sha1" for name in list_crypt_handlers(): if name.startswith(DJANGO_PASSLIB_PREFIX) or name in _other_django_hashes: handler = get_crypt_handler(name) if getattr(handler, "django_name", None) == hasher_name: return name # XXX: this should only happen for custom hashers that have been registered. # _HasherHandler (below) is work in progress that would fix this. raise ValueError("can't translate hasher name to passlib name: %r" % hasher_name)
def __init__(self, name, samples=1): # #get handler, extract boundary information # self.handler = handler = get_crypt_handler(name) if 'rounds' not in handler.setting_kwds: raise ValueError("scheme does not support rounds: %r" % (handler.name,)) self.min_rounds = getattr(handler, "min_rounds", 2) self.max_rounds = getattr(handler, "max_rounds", (1<<32)-1) rc = self.rounds_cost = getattr(handler, "rounds_cost", "linear") # #set up functions that vary based on rounds cost function # if rc == "linear": def get_rps(rounds, delta): return rounds/delta def guess_rounds(rps, target): return int(rps*target+.5) erradj = 2 elif rc == "log2": def get_rps(rounds, delta): return (2**rounds)/delta def guess_rounds(rps, target): return int(logb(rps*target,2)+.5) erradj = 1.1 else: raise NotImplementedError("unknown rounds cost function: %r" % (rc,)) self.get_rps = get_rps self.guess_rounds = guess_rounds self.erradj = erradj # #init cache # self.samples = samples self.cache = {} self.srange = range(samples)
import base64 import hashlib import passlib.exc as exc import passlib.utils.handlers as uh from passlib.registry import get_crypt_handler from passlib.utils import to_unicode from passlib.utils.compat import uascii_to_str passlib_bcrypt = get_crypt_handler("bcrypt") class bcrypt_sha1(uh.StaticHandler): name = "bcrypt_sha1" _hash_prefix = u"$bcrypt_sha1$" def _calc_checksum(self, secret): # Hash the secret with sha1 first secret = hashlib.sha1(secret).hexdigest() # Hash it with bcrypt return passlib_bcrypt.encrypt(secret) def to_string(self): assert self.checksum is not None return uascii_to_str(self._hash_prefix + base64.b64encode(self.checksum)) @classmethod
def get_hash_rounds_info(hash_algo='pbkdf2_sha512', target=200): """ Returns the number of rounds required for a single password verification using hash_algo to take target milliseconds on current CPU. """ target = target / 1000.0 hasher = get_crypt_handler(hash_algo) if hasher.rounds_cost == 'log2': # time cost varies logarithmically with rounds parameter, # so speed = (2**rounds) / elapsed def rounds_to_cost(rounds): return 2**rounds def cost_to_rounds(cost): return math.log(cost, 2) elif hasher.rounds_cost == 'linear': rounds_to_cost = cost_to_rounds = lambda value: value else: raise ValueError( 'Don\'t know how to handle hasher.rounds_cost `{}`'.format( hasher.rounds_cost)) def clamp_rounds(rounds): """ Convert float rounds to int value, clamped to hasher's limits. """ if hasher.max_rounds and rounds > hasher.max_rounds: rounds = hasher.max_rounds rounds = int(rounds) if getattr(hasher, '_avoid_even_rounds', False): rounds |= 1 return max(hasher.min_rounds, rounds) def average(seq): if not hasattr(seq, '__length__'): seq = tuple(seq) return sum(seq) / len(seq) def estimate_iters_per_sec(rounds): """ Estimate performance in interations/s using specified # of rounds. """ secret = '3.14159 26535 89793 23846' # π number hash = hasher.using(rounds=rounds).hash(secret) def helper(): start = timer() hasher.verify(secret, hash) return timer() - start elapsed = min(average(helper() for _ in range(4)) for _ in range(4)) return rounds_to_cost(rounds) / elapsed #--------------------------------------------------------------- # Get rough estimate of performance using fraction of default_rounds # (so we don't take crazy long amounts of time on slow systems) #--------------------------------------------------------------- rounds = clamp_rounds( cost_to_rounds(.5 * rounds_to_cost(hasher.default_rounds))) iters_per_sec = estimate_iters_per_sec(rounds) #--------------------------------------------------------------- # Re-do estimate using previous result, # to get more accurate sample using a larger number of rounds. #--------------------------------------------------------------- for _ in range(2): rounds = clamp_rounds(cost_to_rounds(iters_per_sec * target)) iters_per_sec = estimate_iters_per_sec(rounds) # Actual value to use in password hashing rounds = cost_to_rounds(iters_per_sec * target) # Metadata about current CPU the computation was run on cpu_info = get_cpu_info() return { 'iters_per_sec': int(iters_per_sec), 'rounds': int(rounds), 'cpu_info': { 'vendor_id': cpu_info['vendor_id'], 'brand': cpu_info['brand'], 'hz_advertised': cpu_info['hz_advertised'], 'hz_actual': cpu_info['hz_actual'] } }
def django_to_passlib(self, django_name, cached=True): """ Convert Django hasher / name to Passlib hasher / name. If present, CryptContext will be checked instead of main registry. :param django_name: Django hasher class or algorithm name. "default" allowed if context provided. :raises ValueError: if can't resolve hasher. :returns: passlib hasher or name """ # check for django hasher if hasattr(django_name, "algorithm"): # check for passlib adapter if isinstance(django_name, _PasslibHasherWrapper): return django_name.passlib_handler # resolve django hasher -> name django_name = django_name.algorithm # check cache if cached: cache = self._passlib_hasher_cache try: return cache[django_name] except KeyError: pass result = cache[django_name] = \ self.django_to_passlib(django_name, cached=False) return result # check if it's an obviously-wrapped name if django_name.startswith(PASSLIB_WRAPPER_PREFIX): passlib_name = django_name[len(PASSLIB_WRAPPER_PREFIX):] return self._get_passlib_hasher(passlib_name) # resolve default if django_name == "default": context = self.context if context is None: raise TypeError("can't determine default scheme w/ context") return context.handler() # special case: Django uses a separate hasher for "sha1$$digest" # hashes (unsalted_sha1) and "sha1$salt$digest" (sha1); # but passlib uses "django_salted_sha1" for both of these. if django_name == "unsalted_sha1": django_name = "sha1" # resolve name # XXX: bother caching these lists / mapping? # not needed in long-term due to cache above. context = self.context if context is None: # check registry # TODO: should make iteration via registry easier candidates = ( registry.get_crypt_handler(passlib_name) for passlib_name in registry.list_crypt_handlers() if passlib_name.startswith(DJANGO_COMPAT_PREFIX) or passlib_name in _other_django_hashes ) else: # check context candidates = context.schemes(resolve=True) for handler in candidates: if getattr(handler, "django_name", None) == django_name: return handler # give up # NOTE: this should only happen for custom django hashers that we don't # know the equivalents for. _HasherHandler (below) is work in # progress that would allow us to at least return a wrapper. raise ValueError("can't translate django name to passlib name: %r" % (django_name,))
def _from_dict(self, kwds): "configure policy from constructor keywords" # #init cache & options # context_options = {} options = self._options = {None:{"context":context_options}} self._cache = {} # #normalize & sort keywords # for cat, name, opt, value in parse_policy_items(kwds): copts = options.get(cat) if copts is None: copts = options[cat] = {} config = copts.get(name) if config is None: copts[name] = {opt:value} else: config[opt] = value # #parse list of schemes, and resolve to handlers. # schemes = context_options.get("schemes") or [] handlers = self._handlers = [] handler_names = set() for scheme in schemes: #resolve & validate handler if is_crypt_handler(scheme): handler = scheme else: handler = get_crypt_handler(scheme) name = handler.name if not name: raise TypeError("handler lacks name: %r" % (handler,)) #check name hasn't been re-used if name in handler_names: #XXX: should this just be a warning ? raise KeyError("multiple handlers with same name: %r" % (name,)) #add to handler list handlers.append(handler) handler_names.add(name) # #build _deprecated & _default maps # dmap = self._deprecated = {} fmap = self._default = {} mvmap = self._min_verify_time = {} for cat, config in options.iteritems(): kwds = config.pop("context", None) if not kwds: continue #list of deprecated schemes deps = kwds.get("deprecated") or [] if deps: if handlers: for scheme in deps: if scheme not in handler_names: raise KeyError("known scheme in deprecated list: %r" % (scheme,)) dmap[cat] = frozenset(deps) #default scheme fb = kwds.get("default") if fb: if handlers: if hasattr(fb, "name"): fb = fb.name if fb not in handler_names: raise KeyError("unknown scheme set as default: %r" % (fb,)) fmap[cat] = self.get_handler(fb, required=True) else: fmap[cat] = fb #min verify time value = kwds.get("min_verify_time") if value: mvmap[cat] = value
return self.method.name @property def class_name(self): return get_class_name(self.method) @property def description(self): return get_description(self.method) @property def require_password(self): return requires_password(self.method) @property def require_user(self): return requires_user(self.method) @property def supported(self): return is_supported(self.method) def __call__(self, password, **params): return make_hash(self.method, password, **params) # Init the hash mappings: # attr_name -> MethodWrapper(attr_name, implementation_class) methods = OrderedDict((name, MethodWrapper(get_crypt_handler(name))) for name in list_crypt_handlers())
def __enter__(self): from passlib import registry registry._unload_handler_name(self.name, locations=False) registry.register_crypt_handler(self.dummy) assert registry.get_crypt_handler(self.name) is self.dummy return self.dummy
import base64 import hashlib import passlib.exc as exc import passlib.utils.handlers as uh from passlib.registry import get_crypt_handler from passlib.utils import to_unicode from passlib.utils.compat import uascii_to_str passlib_bcrypt = get_crypt_handler("bcrypt") class bcrypt_sha1(uh.StaticHandler): name = "bcrypt_sha1" _hash_prefix = u"$bcrypt_sha1$" def _calc_checksum(self, secret): # Hash the secret with sha1 first secret = hashlib.sha1(secret).hexdigest() # Hash it with bcrypt return passlib_bcrypt.hash(secret) def to_string(self): assert self.checksum is not None return uascii_to_str(self._hash_prefix + base64.b64encode(self.checksum)) @classmethod
def test_crypt(self): """test crypt.crypt() wrappers""" from passlib.utils import has_crypt, safe_crypt, test_crypt from passlib.registry import get_supported_os_crypt_schemes, get_crypt_handler # test everything is disabled supported = get_supported_os_crypt_schemes() if not has_crypt: self.assertEqual(supported, ()) self.assertEqual(safe_crypt("test", "aa"), None) self.assertFalse(test_crypt("test", "aaqPiZY5xR5l.")) # des_crypt() hash of "test" raise self.skipTest("crypt.crypt() not available") # expect there to be something supported, if crypt() is present if not supported: # NOTE: failures here should be investigated. usually means one of: # 1) at least one of passlib's os_crypt detection routines is giving false negative # 2) crypt() ONLY supports some hash alg which passlib doesn't know about # 3) crypt() is present but completely disabled (never encountered this yet) raise self.fail("crypt() present, but no supported schemes found!") # pick cheap alg if possible, with minimum rounds, to speed up this test. # NOTE: trusting hasher class works properly (should have been verified using it's own UTs) for scheme in ("md5_crypt", "sha256_crypt"): if scheme in supported: break else: scheme = supported[-1] hasher = get_crypt_handler(scheme) if getattr(hasher, "min_rounds", None): hasher = hasher.using(rounds=hasher.min_rounds) # helpers to generate hashes & config strings to work with def get_hash(secret): assert isinstance(secret, unicode) hash = hasher.hash(secret) if isinstance(hash, bytes): # py2 hash = hash.decode("utf-8") assert isinstance(hash, unicode) return hash # test ascii password & return type s1 = u("test") h1 = get_hash(s1) result = safe_crypt(s1, h1) self.assertIsInstance(result, unicode) self.assertEqual(result, h1) self.assertEqual(safe_crypt(to_bytes(s1), to_bytes(h1)), h1) # make sure crypt doesn't just blindly return h1 for whatever we pass in h1x = h1[:-2] + 'xx' self.assertEqual(safe_crypt(s1, h1x), h1) # test utf-8 / unicode password s2 = u('test\u1234') h2 = get_hash(s2) self.assertEqual(safe_crypt(s2, h2), h2) self.assertEqual(safe_crypt(to_bytes(s2), to_bytes(h2)), h2) # test rejects null chars in password self.assertRaises(ValueError, safe_crypt, '\x00', h1) # check test_crypt() self.assertTrue(test_crypt("test", h1)) self.assertFalse(test_crypt("test", h1x)) # check crypt returning variant error indicators # some platforms return None on errors, others empty string, # The BSDs in some cases return ":" import passlib.utils as mod orig = mod._crypt try: retval = None mod._crypt = lambda secret, hash: retval for retval in [None, "", ":", ":0", "*0"]: self.assertEqual(safe_crypt("test", h1), None) self.assertFalse(test_crypt("test", h1)) retval = 'xxx' self.assertEqual(safe_crypt("test", h1), "xxx") self.assertFalse(test_crypt("test", h1)) finally: mod._crypt = orig
def main(*args): #--------------------------------------------------------------- # parse args #--------------------------------------------------------------- args = list(args) def print_error(msg): print "error: %s\n" % msg # parse hasher if args: name = args.pop(0) if name == "-h" or name == "--help": print _usage return 1 try: hasher = get_crypt_handler(name) except KeyError: print_error("unknown hash %r" % name) return 1 if 'rounds' not in hasher.setting_kwds: print_error("%s does not support variable rounds" % name) return 1 else: print_error("hash name not specified") print _usage return 1 # parse target time if args: try: target = int(args.pop(0)) * .001 if target <= 0: raise ValueError except ValueError: print_error("target time must be integer milliseconds > 0") return 1 else: target = .350 #--------------------------------------------------------------- # setup some helper functions #--------------------------------------------------------------- if hasher.rounds_cost == "log2": # time cost varies logarithmically with rounds parameter, # so speed = (2**rounds) / elapsed def rounds_to_cost(rounds): return 2**rounds def cost_to_rounds(cost): return math.log(cost, 2) else: # time cost varies linearly with rounds parameter, # so speed = rounds / elapsed assert hasher.rounds_cost == "linear" rounds_to_cost = cost_to_rounds = lambda value: value def clamp_rounds(rounds): """convert float rounds to int value, clamped to hasher's limits""" if hasher.max_rounds and rounds > hasher.max_rounds: rounds = hasher.max_rounds rounds = int(rounds) if getattr(hasher, "_avoid_even_rounds", False): rounds |= 1 return max(hasher.min_rounds, rounds) def average(seq): if not hasattr(seq, "__length__"): seq = tuple(seq) return sum(seq) / len(seq) def estimate_speed(rounds): """estimate speed using specified # of rounds""" # time a single verify() call secret = "S0m3-S3Kr1T" hash = hasher.encrypt(secret, rounds=rounds) def helper(): start = tick() hasher.verify(secret, hash) return tick() - start # try to get average time over a few samples # XXX: way too much variability between sampling runs, # would like to improve this bit elapsed = min(average(helper() for _ in range(4)) for _ in range(4)) return rounds_to_cost(rounds) / elapsed #--------------------------------------------------------------- # get rough estimate of speed using fraction of default_rounds # (so we don't take crazy long amounts of time on slow systems) #--------------------------------------------------------------- rounds = clamp_rounds( cost_to_rounds(.5 * rounds_to_cost(hasher.default_rounds))) speed = estimate_speed(rounds) #--------------------------------------------------------------- # re-do estimate using previous result, # to get more accurate sample using a larger number of rounds. #--------------------------------------------------------------- for _ in range(2): rounds = clamp_rounds(cost_to_rounds(speed * target)) speed = estimate_speed(rounds) #--------------------------------------------------------------- # using final estimate, calc desired number of rounds for target time #--------------------------------------------------------------- if hasattr(hasher, "backends"): name = "%s (using %s backend)" % (name, hasher.get_backend()) print "hash............: %s" % name if speed < 1000: speedstr = "%.2f" % speed else: speedstr = int(speed) print "speed...........: %s iterations/second" % speedstr print "target time.....: %d ms" % (target * 1000, ) rounds = cost_to_rounds(speed * target) if hasher.rounds_cost == "log2": # for log2 rounds parameter, target time will usually fall # somewhere between two integer values, which will have large gulf # between them. if target is within <tolerance> percent of # one of two ends, report it, otherwise list both and let user decide. tolerance = .05 lower = clamp_rounds(rounds) upper = clamp_rounds(math.ceil(rounds)) lower_elapsed = rounds_to_cost(lower) / speed upper_elapsed = rounds_to_cost(upper) / speed if (target - lower_elapsed) / target < tolerance: print "target rounds...: %d" % lower elif (upper_elapsed - target) / target < tolerance: print "target rounds...: %d" % upper else: print "target rounds...: %d (%dms -- %dms faster than requested)" % \ (lower, lower_elapsed*1000, (target - lower_elapsed) * 1000) print "target rounds...: %d (%dms -- %dms slower than requested)" % \ (upper, upper_elapsed*1000, (upper_elapsed - target) * 1000) else: # for linear rounds parameter, just use nearest integer value rounds = clamp_rounds(round(rounds)) print "target rounds...: %d" % (rounds, ) print
def passlib_to_hasher_name(passlib_name): """convert passlib handler name -> hasher name""" handler = get_crypt_handler(passlib_name) if hasattr(handler, "django_name"): return handler.django_name return PASSLIB_HASHER_PREFIX + passlib_name
def main(*args): #--------------------------------------------------------------- # parse args #--------------------------------------------------------------- args = list(args) def print_error(msg): print "error: %s\n" % msg # parse hasher if args: name = args.pop(0) if name == "-h" or name == "--help": print _usage return 1 try: hasher = get_crypt_handler(name) except KeyError: print_error("unknown hash %r" % name) return 1 if 'rounds' not in hasher.setting_kwds: print_error("%s does not support variable rounds" % name) return 1 else: print_error("hash name not specified") print _usage return 1 # parse target time if args: try: target = int(args.pop(0))*.001 if target <= 0: raise ValueError except ValueError: print_error("target time must be integer milliseconds > 0") return 1 else: target = .350 #--------------------------------------------------------------- # setup some helper functions #--------------------------------------------------------------- if hasher.rounds_cost == "log2": # time cost varies logarithmically with rounds parameter, # so speed = (2**rounds) / elapsed def rounds_to_cost(rounds): return 2 ** rounds def cost_to_rounds(cost): return math.log(cost, 2) else: # time cost varies linearly with rounds parameter, # so speed = rounds / elapsed assert hasher.rounds_cost == "linear" rounds_to_cost = cost_to_rounds = lambda value: value def clamp_rounds(rounds): """convert float rounds to int value, clamped to hasher's limits""" if hasher.max_rounds and rounds > hasher.max_rounds: rounds = hasher.max_rounds rounds = int(rounds) if getattr(hasher, "_avoid_even_rounds", False): rounds |= 1 return max(hasher.min_rounds, rounds) def average(seq): if not hasattr(seq, "__length__"): seq = tuple(seq) return sum(seq) / len(seq) def estimate_speed(rounds): """estimate speed using specified # of rounds""" # time a single verify() call secret = "S0m3-S3Kr1T" hash = hasher.encrypt(secret, rounds=rounds) def helper(): start = tick() hasher.verify(secret, hash) return tick() - start # try to get average time over a few samples # XXX: way too much variability between sampling runs, # would like to improve this bit elapsed = min(average(helper() for _ in range(4)) for _ in range(4)) return rounds_to_cost(rounds) / elapsed #--------------------------------------------------------------- # get rough estimate of speed using fraction of default_rounds # (so we don't take crazy long amounts of time on slow systems) #--------------------------------------------------------------- rounds = clamp_rounds(cost_to_rounds(.5 * rounds_to_cost(hasher.default_rounds))) speed = estimate_speed(rounds) #--------------------------------------------------------------- # re-do estimate using previous result, # to get more accurate sample using a larger number of rounds. #--------------------------------------------------------------- for _ in range(2): rounds = clamp_rounds(cost_to_rounds(speed * target)) speed = estimate_speed(rounds) #--------------------------------------------------------------- # using final estimate, calc desired number of rounds for target time #--------------------------------------------------------------- if hasattr(hasher, "backends"): name = "%s (using %s backend)" % (name, hasher.get_backend()) print "hash............: %s" % name if speed < 1000: speedstr = "%.2f" % speed else: speedstr = int(speed) print "speed...........: %s iterations/second" % speedstr print "target time.....: %d ms" % (target*1000,) rounds = cost_to_rounds(speed * target) if hasher.rounds_cost == "log2": # for log2 rounds parameter, target time will usually fall # somewhere between two integer values, which will have large gulf # between them. if target is within <tolerance> percent of # one of two ends, report it, otherwise list both and let user decide. tolerance = .05 lower = clamp_rounds(rounds) upper = clamp_rounds(math.ceil(rounds)) lower_elapsed = rounds_to_cost(lower) / speed upper_elapsed = rounds_to_cost(upper) / speed if (target-lower_elapsed)/target < tolerance: print "target rounds...: %d" % lower elif (upper_elapsed-target)/target < tolerance: print "target rounds...: %d" % upper else: print "target rounds...: %d (%dms -- %dms faster than requested)" % \ (lower, lower_elapsed*1000, (target - lower_elapsed) * 1000) print "target rounds...: %d (%dms -- %dms slower than requested)" % \ (upper, upper_elapsed*1000, (upper_elapsed - target) * 1000) else: # for linear rounds parameter, just use nearest integer value rounds = clamp_rounds(round(rounds)) print "target rounds...: %d" % (rounds,) print
def django_to_passlib(self, django_name, cached=True): """ Convert Django hasher / name to Passlib hasher / name. If present, CryptContext will be checked instead of main registry. :param django_name: Django hasher class or algorithm name. "default" allowed if context provided. :raises ValueError: if can't resolve hasher. :returns: passlib hasher or name """ # check for django hasher if hasattr(django_name, "algorithm"): # check for passlib adapter if isinstance(django_name, _PasslibHasherWrapper): return django_name.passlib_handler # resolve django hasher -> name django_name = django_name.algorithm # check cache if cached: cache = self._passlib_hasher_cache try: return cache[django_name] except KeyError: pass result = cache[django_name] = self.django_to_passlib(django_name, cached=False) return result # check if it's an obviously-wrapped name if django_name.startswith(PASSLIB_WRAPPER_PREFIX): passlib_name = django_name[len(PASSLIB_WRAPPER_PREFIX):] return self._get_passlib_hasher(passlib_name) # resolve default if django_name == "default": context = self.context if context is None: raise TypeError("can't determine default scheme w/ context") return context.handler() # special case: Django uses a separate hasher for "sha1$$digest" # hashes (unsalted_sha1) and "sha1$salt$digest" (sha1); # but passlib uses "django_salted_sha1" for both of these. if django_name == "unsalted_sha1": django_name = "sha1" # resolve name # XXX: bother caching these lists / mapping? # not needed in long-term due to cache above. context = self.context if context is None: # check registry # TODO: should make iteration via registry easier candidates = (registry.get_crypt_handler(passlib_name) for passlib_name in registry.list_crypt_handlers() if passlib_name.startswith(DJANGO_COMPAT_PREFIX) or passlib_name in _other_django_hashes) else: # check context candidates = context.schemes(resolve=True) for handler in candidates: if getattr(handler, "django_name", None) == django_name: return handler # give up # NOTE: this should only happen for custom django hashers that we don't # know the equivalents for. _HasherHandler (below) is work in # progress that would allow us to at least return a wrapper. raise ValueError("can't translate django name to passlib name: %r" % (django_name, ))