From 3af66892ada79ce61bd12d50fef341ac506b2279 Mon Sep 17 00:00:00 2001 From: Eli Collins Date: Thu, 3 Mar 2011 18:11:59 -0500 Subject: cryptcontext work ================= * hash_needs_update() method - renamed method from hash_is_compliant() - cleaned up code - added UT for method * prepare_settings() methods - renamed method from norm_handler_settings() - cleaned up code - TODO: write UT * renamed "vary_default_rounds" to "vary_rounds", was too verbose --- docs/lib/passlib.base.rst | 14 +++--- passlib/base.py | 113 ++++++++++++++++++++++----------------------- passlib/tests/test_base.py | 44 ++++++++++++++++-- 3 files changed, 102 insertions(+), 69 deletions(-) diff --git a/docs/lib/passlib.base.rst b/docs/lib/passlib.base.rst index 14cce6f..1c32c0a 100644 --- a/docs/lib/passlib.base.rst +++ b/docs/lib/passlib.base.rst @@ -50,7 +50,7 @@ The remaining options - ``context.deprecated`` comma separated list of the schemes which this context should recognize, - generated hashes only if explicitly requested, and for which ``context.is_compliant()`` should return ``False``. + generated hashes only if explicitly requested, and for which ``context.hash_needs_update()`` should return ``False``. if not specified, none are considered deprecated. this must be a subset of the names listed in context.schemes @@ -71,7 +71,7 @@ The remaining options - * these are configurable per-context limits, hard limits set by algorithm are always applied * if min > max, max will be increased to equal min. * ``context.genconfig()`` or ``config.encrypt()`` - requests outside of these bounds will be clipped. - * ``context.is_compliant()`` - existing hashes w/ rounds outside of range are not compliant + * ``context.hash_needs_update()`` - existing hashes w/ rounds outside of range are not compliant * for hashes which do not have a rounds parameter, these values are ignored. ``{hash}.default_rounds`` @@ -83,16 +83,16 @@ The remaining options - * for hashes which do not have a rounds parameter, this value is ignored. * if not specified, max_rounds is used if available, then min_rounds, then the algorithm default. -``{hash}.vary_default_rounds`` +``{hash}.vary_rounds`` [only applies if ``{hash}.default_rounds`` is specified and > 0] if specified, every time a new hash is created using {hash}/default_rounds for it's rounds value, the actual value used is generated at random, using default_rounds as a hint. - * integer value - a value will be chosen using the formula ``randint(default_rounds-vary_default_rounds, default_rounds+vary_default_rounds)``. - * integer value between 0 and 100 with ``%`` suffix - same as above, with integer value equal to ``vary_default_rounds*default_rounds/100``. - * note that if algorithms indicate they use a logarthmic rounds parameter, the percent syntax equation uses ``log(vary_default_rounds*(2**default_rounds)/100,2)``, + * integer value - a value will be chosen using the formula ``randint(default_rounds-vary_rounds, default_rounds+vary_rounds)``. + * integer value between 0 and 100 with ``%`` suffix - same as above, with integer value equal to ``vary_rounds*default_rounds/100``. + * note that if algorithms indicate they use a logarthmic rounds parameter, the percent syntax equation uses ``log(vary_rounds*(2**default_rounds)/100,2)``, to permit a default value to be applicable to all schemes. XXX: this might be a bad / overly complex idea. ``{hash}.{setting}`` @@ -138,7 +138,7 @@ A sample policy file:: min_verify_time = 0.1 #set some common options for all schemes - all.vary_default_rounds = 10% + all.vary_rounds = 10% #setup some hash-specific defaults sha512_crypt.min_rounds = 40000 diff --git a/passlib/base.py b/passlib/base.py index 9244e19..9c28a3e 100644 --- a/passlib/base.py +++ b/passlib/base.py @@ -383,6 +383,7 @@ class CryptPolicy(object): #========================================================= def _from_dict(self, kwds): "configure policy from constructor keywords" + # #normalize & sort keywords # @@ -759,64 +760,56 @@ class CryptContext(object): names = [ handler.name for handler in self.policy.iter_handlers() ] return "" % (id(self), names) - ##def replace(self, *args, **kwds): - ## "return CryptContext with new policy which has specified values replaced" - ## return CryptContext(policy=self.policy.replace(*args,**kwds)) - #=================================================================== #policy adaptation #=================================================================== - ##def get_handler(self, name=None, category=None, required=False): - ## """given an algorithm name, return CryptHandler instance which manages it. - ## if no match is found, returns None. - ## - ## if name is None, will return default algorithm - ## """ - ## return self.policy.get_handler(name, category, required) - - def norm_handler_settings(self, handler, category=None, **settings): + def _prepare_rounds(self, handler, opts, settings): + "helper for prepare_default_settings" + mn = opts.get("min_rounds") + mx = opts.get("max_rounds") + rounds = settings.get("rounds") + if rounds is None: + df = opts.get("default_rounds") or mx or mn + if df is not None: + vr = opts.get("vary_rounds") + if vr: + if isinstance(vr, str) and vr.endswith("%"): + rc = getattr(handler, "rounds_cost", "linear") + vr = int(vr[:-1]) + assert 0 <= vr < 100 + if rc == "log2": #let % variance scale the linear number of rounds, not the log rounds cost + vr = int(logb(vr*.01*(2**df),2)+.5) + else: + vr = int(df*vr/100) + rounds = rng.randint(df-vr,df+vr) + else: + rounds = df + if rounds is not None: + if mx and rounds > mx: + rounds = mx + if mn and rounds < mn: #give mn predence if mn > mx + rounds = mn + settings['rounds'] = rounds + + def prepare_settings(self, handler, category=None, **settings): "normalize settings for handler according to context configuration" opts = self.policy.get_options(handler, category) if not opts: return settings - #load in default values + #load in default values for any settings for k in handler.setting_kwds: if k not in settings and k in opts: settings[k] = opts[k] #handle rounds if 'rounds' in handler.setting_kwds: - #TODO: prep-parse & validate this w/in get_options() ? - mn = opts.get("min_rounds") - mx = opts.get("max_rounds") - rounds = settings.get("rounds") - if rounds is None: - df = opts.get("default_rounds") or mx or mn - if df is not None: - vr = opts.get("vary_default_rounds") - if vr: - if isinstance(vr, str) and vr.endswith("%"): - rc = getattr(handler, "rounds_cost", "linear") - vr = int(vr[:-1]) - assert 0 <= vr < 100 - if rc == "log2": #let % variance scale the linear number of rounds, not the log rounds cost - vr = int(logb(vr*.01*(2**df),2)+.5) - else: - vr = int(df*vr/100) - rounds = rng.randint(df-vr,df+vr) - else: - rounds = df - if rounds is not None: - if mx and rounds > mx: - rounds = mx - if mn and rounds < mn: #give mn predence if mn > mx - rounds = mn - settings['rounds'] = rounds + self._prepare_rounds(handler, opts, settings) + #done return settings - def hash_is_compliant(self, hash, category=None): + def hash_needs_update(self, hash, category=None): """check if hash is allowed by current policy, or if secret should be re-encrypted""" handler = self.identify(hash, resolve=True, required=True) policy = self.policy @@ -827,23 +820,25 @@ class CryptContext(object): #get options, and call compliance helper (check things such as rounds, etc) opts = policy.get_options(handler, category) - if not opts: - return False - - #XXX: could check if handler provides it's own helper, eg getattr(handler, "is_compliant", None) - - if hasattr(handler, "from_string"): - info = handler.from_string(hash) - rounds = getattr(info, "rounds", None) - if rounds is not None: - min_rounds = opts.get("min_rounds") - if min_rounds and rounds < min_rounds: - return False - max_rounds = opts.get("max_rounds") - if max_rounds and rounds > max_rounds: - return False - return compliance_helper(handler, hash, **opts) + #XXX: could check if handler provides it's own helper, eg getattr(handler, "hash_needs_update", None), + #and call that instead of the following default behavior + + if opts: + #check if we can parse hash to check it's rounds parameter + if ('min_rounds' in opts or 'max_rounds' in opts) and \ + 'rounds' in handler.setting_kwds and hasattr(handler, "from_string"): + info = handler.from_string(hash) + rounds = getattr(info, "rounds", None) + if rounds is not None: + min_rounds = opts.get("min_rounds") + if min_rounds and rounds < min_rounds: + return True + max_rounds = opts.get("max_rounds") + if max_rounds and rounds > max_rounds: + return True + + return False #=================================================================== #password hash api proxy methods @@ -851,7 +846,7 @@ class CryptContext(object): def genconfig(self, scheme=None, category=None, **settings): """Call genconfig() for specified handler""" handler = self.policy.get_handler(scheme, category, required=True) - settings = self.norm_handler_settings(handler, category, **settings) + settings = self.prepare_settings(handler, category, **settings) return handler.genconfig(**settings) def genhash(self, secret, config, scheme=None, category=None, **context): @@ -918,7 +913,7 @@ class CryptContext(object): if not self: raise ValueError, "no algorithms registered" handler = self.policy.get_handler(scheme, category, required=True) - kwds = self.norm_handler_settings(handler, category, **kwds) + kwds = self.prepare_settings(handler, category, **kwds) #XXX: could insert normalization to preferred unicode encoding here return handler.encrypt(secret, **kwds) diff --git a/passlib/tests/test_base.py b/passlib/tests/test_base.py index 1275f0d..f76a9f5 100644 --- a/passlib/tests/test_base.py +++ b/passlib/tests/test_base.py @@ -340,9 +340,47 @@ class CryptContextTest(TestCase): #========================================================= #policy adaptation #========================================================= - #TODO: - #norm_handler_settings - #hash_is_compliant + sample_policy_1 = dict( + schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha256_crypt"], + deprecated = [ "des_crypt", ], + default = "sha256_crypt", + all__vary_rounds = "10%", + bsdi_crypt__max_rounds = 30, + bsdi_crypt__default_rounds = 25, + bsdi_crypt__vary_rounds = 0, + sha256_crypt__max_rounds = 3000, + sha256_crypt__min_rounds = 2000, + ) + + def test_10_prepare_settings(self): + "test prepare_settings() method" + cc = CryptContext(**self.sample_policy_1) + + #TODO: + # hash specific settings + # min rounds + # max rounds + # default rounds + # falls back to max, then min. + # specified + # outside of min/max range + # default+vary rounds + + def test_10_hash_needs_update(self): + "test hash_needs_update() method" + cc = CryptContext(**self.sample_policy_1) + + #check deprecated scheme + self.assert_(cc.hash_needs_update('9XXD4trGYeGJA')) + self.assert_(not cc.hash_needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0')) + + #check min rounds + self.assert_(cc.hash_needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/')) + self.assert_(not cc.hash_needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8')) + + #check max rounds + self.assert_(not cc.hash_needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.')) + self.assert_(cc.hash_needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA')) #========================================================= #identify -- cgit v1.2.1