summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-03-03 18:11:59 -0500
committerEli Collins <elic@assurancetechnologies.com>2011-03-03 18:11:59 -0500
commit3af66892ada79ce61bd12d50fef341ac506b2279 (patch)
treeb44f525cb24559c3c9975c092927af4d5f5cd5c6
parentf7bc802518391d6ed003afb1d061f5ebe0c04d88 (diff)
downloadpasslib-3af66892ada79ce61bd12d50fef341ac506b2279.tar.gz
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
-rw-r--r--docs/lib/passlib.base.rst14
-rw-r--r--passlib/base.py113
-rw-r--r--passlib/tests/test_base.py44
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 "<CryptContext %0xd schemes=%r>" % (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