diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2012-04-27 00:49:55 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2012-04-27 00:49:55 -0400 |
commit | 03964d1fce1720413498dbb537acc52ba3321566 (patch) | |
tree | 0ee97040ecc6c0c930e4feb04dc6c3406f9af079 | |
parent | a8c0ef90542ec5b17fe84c18ecbe7c439da4105d (diff) | |
download | passlib-03964d1fce1720413498dbb537acc52ba3321566.tar.gz |
context change - shortened hash_needs_update() to needs_update()
- renamed internal update hooks as well
- needs_update() now accepts an optional copy of the password.
this is unused for now, but should pave the way for properly
migrating crypt_blowfish $2x$ hashes in the next release.
-rw-r--r-- | passlib/context.py | 50 | ||||
-rw-r--r-- | passlib/handlers/bcrypt.py | 7 | ||||
-rw-r--r-- | passlib/handlers/des_crypt.py | 6 | ||||
-rw-r--r-- | passlib/handlers/scram.py | 4 | ||||
-rw-r--r-- | passlib/tests/test_context.py | 37 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 6 | ||||
-rw-r--r-- | passlib/utils/handlers.py | 4 |
7 files changed, 64 insertions, 50 deletions
diff --git a/passlib/context.py b/passlib/context.py index b0b4d12..3d14238 100644 --- a/passlib/context.py +++ b/passlib/context.py @@ -630,8 +630,8 @@ class _CryptRecord(object): # verify() attrs _min_verify_time = None - # hash_needs_update() attrs - _is_deprecated_by_handler = None # optional callable used by bcrypt/scram + # needs_update() attrs + _needs_update = None # optional callable provided by handler _has_rounds_introspection = False # if rounds can be extract from hash # cloned directly from handler, not affected by config options. @@ -658,7 +658,7 @@ class _CryptRecord(object): # init wrappers for handler methods we modify args to self._init_encrypt_and_genconfig() self._init_verify(min_verify_time) - self._init_hash_needs_update() + self._init_needs_update() # these aren't wrapped by _CryptRecord, copy them directly from handler. self.identify = handler.identify @@ -931,44 +931,44 @@ class _CryptRecord(object): return False #================================================================ - # hash_needs_update() + # needs_update() #================================================================ - def _init_hash_needs_update(self): - """initialize state for hash_needs_update()""" + def _init_needs_update(self): + """initialize state for needs_update()""" # if handler has been deprecated, replace wrapper and skip other checks if self.deprecated: - self.hash_needs_update = lambda hash: True + self.needs_update = lambda hash, secret: True return # let handler detect hashes with configurations that don't match # current settings. currently do this by calling - # ``handler._deprecation_detector(**settings)``, which if defined - # should return None or a callable ``is_deprecated(hash)->bool``. + # ``handler._bind_needs_update(**settings)``, which if defined + # should return None or a callable ``needs_update(hash,secret)->bool``. # # NOTE: this interface is still private, because it was hacked in # for the sake of bcrypt & scram, and is subject to change. handler = self.handler - const = getattr(handler, "_deprecation_detector", None) + const = getattr(handler, "_bind_needs_update", None) if const: - self._is_deprecated_by_handler = const(**self.settings) + self._needs_update = const(**self.settings) # XXX: what about a "min_salt_size" deprecator? # set flag if we can extract rounds from hash, allowing - # hash_needs_update() to check for rounds that are outside of + # needs_update() to check for rounds that are outside of # the configured range. if self._has_rounds_bounds and hasattr(handler, "from_string"): self._has_rounds_introspection = True - def hash_needs_update(self, hash): + def needs_update(self, hash, secret): # init replaces this method entirely for this case. ### check if handler has been deprecated ##if self.deprecated: ## return True # check handler's detector if it provided one. - check = self._is_deprecated_by_handler - if check and check(hash): + check = self._needs_update + if check and check(hash, secret): return True # if we can parse rounds parameter, check if it's w/in bounds. @@ -1078,7 +1078,7 @@ class CryptContext(object): Applications which want to detect and re-encrypt deprecated hashes will want to use one of the following methods: - .. automethod:: hash_needs_update + .. automethod:: needs_update .. automethod:: verify_and_update Additionally, the main interface offers a few helper methods, @@ -2215,8 +2215,7 @@ class CryptContext(object): # Each record object stores the options used # by the specific (scheme,category) combination it manages. - # XXX: would a better name be needs_update/is_deprecated? - def hash_needs_update(self, hash, scheme=None, category=None): + def needs_update(self, hash, scheme=None, category=None, secret=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: @@ -2230,11 +2229,20 @@ class CryptContext(object): :arg hash: existing hash string :param scheme: optionally identify specific scheme to check against. :param category: optional user category + :param secret: optional copy of associated password :returns: True/False """ record = self._get_or_identify_record(hash, scheme, category) - return record.hash_needs_update(hash) + return record.needs_update(hash, secret) + + def hash_needs_update(self, hash, scheme=None, category=None): + """legacy alias for :meth:`needs_update`. + + .. deprecated:: 1.6 + use :meth:`needs_update` instead. + """ + return self.needs_update(hash, scheme, category) def genconfig(self, scheme=None, category=None, **settings): """Call genconfig() for specified handler @@ -2362,7 +2370,7 @@ class CryptContext(object): This is a convenience method for a common situation in most applications: When a user logs in, they must :meth:`verify` if the password matches; if successful, check if the hash algorithm - has been deprecated (:meth:`hash_needs_update`); and if so, + has been deprecated (:meth:`needs_update`); and if so, re-:meth:`encrypt` the secret. This method takes care of calling all of these 3 methods, returning a simple tuple for the application to use. @@ -2400,7 +2408,7 @@ class CryptContext(object): record = self._get_or_identify_record(hash, scheme, category) if not record.verify(secret, hash, **kwds): return False, None - elif record.hash_needs_update(hash): + elif record.needs_update(hash, secret): # NOTE: we re-encrypt with default scheme, not current one. return True, self.encrypt(secret, None, category, **kwds) else: diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 4d12091..5df66e3 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -159,16 +159,17 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. #========================================================= @classmethod - def _deprecation_detector(cls, **settings): - return cls._hash_needs_update + def _bind_needs_update(cls, **settings): + return cls._needs_update @classmethod - def _hash_needs_update(cls, hash): + def _needs_update(cls, hash, secret): if isinstance(hash, bytes): hash = hash.decode("ascii") # check for incorrect padding bits (passlib issue 25) if hash.startswith(IDENT_2A) and hash[28] not in bcrypt64._padinfo2[1]: return True + # TODO: try to detect incorrect $2x$ hashes using *secret* return False @classmethod diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py index 4a19532..68ab3dd 100644 --- a/passlib/handlers/des_crypt.py +++ b/passlib/handlers/des_crypt.py @@ -278,11 +278,11 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler return rounds @classmethod - def _deprecation_detector(cls, **settings): - return cls._hash_needs_update + def _bind_needs_update(cls, **settings): + return cls._needs_update @classmethod - def _hash_needs_update(cls, hash): + def _needs_update(cls, hash, secret): # mark bsdi_crypt hashes as deprecated if they have even rounds. assert cls.identify(hash) if isinstance(hash, unicode): diff --git a/passlib/handlers/scram.py b/passlib/handlers/scram.py index 036d7c2..ef73cf4 100644 --- a/passlib/handlers/scram.py +++ b/passlib/handlers/scram.py @@ -323,12 +323,12 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler): #========================================================= @classmethod - def _deprecation_detector(cls, **settings): + def _bind_needs_update(cls, **settings): "generate a deprecation detector for CryptContext to use" # generate deprecation hook which marks hashes as deprecated # if they don't support a superset of current algs. algs = frozenset(cls(use_defaults=True, **settings).algs) - def detector(hash): + def detector(hash, secret): return not algs.issubset(cls.from_string(hash).algs) return detector diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py index 28ce208..2abbccf 100644 --- a/passlib/tests/test_context.py +++ b/passlib/tests/test_context.py @@ -941,21 +941,23 @@ sha512_crypt__min_rounds = 45000 # throws error without schemes self.assertRaises(KeyError, CryptContext().verify, 'secret', 'hash') - def test_46_hash_needs_update(self): - "test hash_needs_update() method" + def test_46_needs_update(self): + "test needs_update() method" cc = CryptContext(**self.sample_4_dict) #check deprecated scheme - self.assertTrue(cc.hash_needs_update('9XXD4trGYeGJA')) - self.assertFalse(cc.hash_needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0')) + self.assertTrue(cc.needs_update('9XXD4trGYeGJA')) + self.assertFalse(cc.needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0')) #check min rounds - self.assertTrue(cc.hash_needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/')) - self.assertFalse(cc.hash_needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8')) + self.assertTrue(cc.needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/')) + self.assertFalse(cc.needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8')) #check max rounds - self.assertFalse(cc.hash_needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.')) - self.assertTrue(cc.hash_needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA')) + self.assertFalse(cc.needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.')) + self.assertTrue(cc.needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA')) + + # TODO: check with secret passed in, that _bind_needs_update() is invoked correctly. #-------------------------------------------------------------- # border cases @@ -964,10 +966,10 @@ sha512_crypt__min_rounds = 45000 # rejects non-string hashes cc = CryptContext(["des_crypt"]) for hash, kwds in self.nonstring_vectors: - self.assertRaises(TypeError, cc.hash_needs_update, hash, **kwds) + self.assertRaises(TypeError, cc.needs_update, hash, **kwds) # throws error without schemes - self.assertRaises(KeyError, CryptContext().hash_needs_update, 'hash') + self.assertRaises(KeyError, CryptContext().needs_update, 'hash') def test_47_verify_and_update(self): "test verify_and_update()" @@ -1322,8 +1324,11 @@ sha512_crypt__min_rounds = 45000 self.assertFalse(ctx.verify(pu, ctx.encrypt(pn, scheme="md5_crypt"))) self.assertTrue(ctx.verify(pu, ctx.encrypt(pn, scheme="sha256_crypt"))) + #========================================================= + # handler deprecation detectors + #========================================================= def test_62_bcrypt_update(self): - "test verify_and_update / hash_needs_update corrects bcrypt padding" + "test verify_and_update / needs_update corrects bcrypt padding" # see issue 25. bcrypt = hash.bcrypt @@ -1333,8 +1338,8 @@ sha512_crypt__min_rounds = 45000 ctx = CryptContext(["bcrypt"]) with catch_warnings(record=True) as wlog: - self.assertTrue(ctx.hash_needs_update(BAD1)) - self.assertFalse(ctx.hash_needs_update(GOOD1)) + self.assertTrue(ctx.needs_update(BAD1)) + self.assertFalse(ctx.needs_update(GOOD1)) if bcrypt.has_backend(): self.assertEqual(ctx.verify_and_update(PASS1,GOOD1), (True,None)) @@ -1344,14 +1349,14 @@ sha512_crypt__min_rounds = 45000 self.assertTrue(new_hash and new_hash != BAD1) def test_63_bsdi_crypt_update(self): - "test verify_and_update / hash_needs_update correct bsdi even rounds" + "test verify_and_update / needs_update corrects bsdi even rounds" even_hash = '_Y/../cG0zkJa6LY6k4c' odd_hash = '_Z/..TgFg0/ptQtpAgws' secret = 'test' ctx = CryptContext(['bsdi_crypt']) - self.assertTrue(ctx.hash_needs_update(even_hash)) - self.assertFalse(ctx.hash_needs_update(odd_hash)) + self.assertTrue(ctx.needs_update(even_hash)) + self.assertFalse(ctx.needs_update(odd_hash)) self.assertEqual(ctx.verify_and_update(secret, odd_hash), (True,None)) self.assertEqual(ctx.verify_and_update("x", even_hash), (False,None)) diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 503b349..4e7c040 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -2046,13 +2046,13 @@ class scram_test(HandlerCase): h = c1.encrypt("dummy") self.assertEqual(handler.extract_digest_algs(h), ["md5", "sha-1"]) - self.assertFalse(c1.hash_needs_update(h)) + self.assertFalse(c1.needs_update(h)) c2 = c1.copy(scram__algs="sha1") - self.assertFalse(c2.hash_needs_update(h)) + self.assertFalse(c2.needs_update(h)) c2 = c1.copy(scram__algs="sha1,sha256") - self.assertTrue(c2.hash_needs_update(h)) + self.assertTrue(c2.needs_update(h)) def test_96_full_verify(self): "test verify(full=True) flag" diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py index eb2b7d3..76de5bc 100644 --- a/passlib/utils/handlers.py +++ b/passlib/utils/handlers.py @@ -563,13 +563,13 @@ class GenericHandler(object): #========================================================= ##@classmethod - ##def _deprecation_detector(cls, **settings): + ##def _bind_needs_update(cls, **settings): ## """return helper to detect deprecated hashes. ## ## if this method is defined, the CryptContext constructor ## will invoke it with the settings specified for the context. ## this method should return None or a callable - ## with the signature ``func(hash)->bool``. + ## with the signature ``func(hash,secret)->bool``. ## ## this function should return true if the hash ## should be re-encrypted, whether due to internal |