diff options
| -rw-r--r-- | docs/lib/passlib.utils.rst | 2 | ||||
| -rw-r--r-- | passlib/handlers/bcrypt.py | 38 | ||||
| -rw-r--r-- | passlib/handlers/des_crypt.py | 46 | ||||
| -rw-r--r-- | passlib/handlers/md5_crypt.py | 9 | ||||
| -rw-r--r-- | passlib/handlers/pbkdf2.py | 8 | ||||
| -rw-r--r-- | passlib/handlers/sha1_crypt.py | 14 | ||||
| -rw-r--r-- | passlib/handlers/sha2_crypt.py | 36 | ||||
| -rw-r--r-- | passlib/handlers/sun_md5_crypt.py | 8 | ||||
| -rw-r--r-- | passlib/hosts.py | 4 | ||||
| -rw-r--r-- | passlib/tests/test_utils.py | 92 | ||||
| -rw-r--r-- | passlib/tests/test_utils_handlers.py | 5 | ||||
| -rw-r--r-- | passlib/tests/utils.py | 52 | ||||
| -rw-r--r-- | passlib/utils/__init__.py | 108 | ||||
| -rw-r--r-- | passlib/utils/handlers.py | 1 |
14 files changed, 220 insertions, 203 deletions
diff --git a/docs/lib/passlib.utils.rst b/docs/lib/passlib.utils.rst index 9ecd913..5508191 100644 --- a/docs/lib/passlib.utils.rst +++ b/docs/lib/passlib.utils.rst @@ -133,7 +133,7 @@ Predefined Instances .. Host OS ======= - .. autofunction:: safe_os_crypt + .. autofunction:: safe_crypt .. autofunction:: tick Randomness diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 9a1f595..7a698d4 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -27,8 +27,8 @@ except ImportError: #pragma: no cover - though should run whole suite w/o bcrypt bcryptor_engine = None #libs from passlib.exc import PasslibHandlerWarning -from passlib.utils import BCRYPT_CHARS as BCHARS, safe_os_crypt, \ - classproperty, rng, getrandstr +from passlib.utils import BCRYPT_CHARS as BCHARS, safe_crypt, \ + classproperty, rng, getrandstr, test_crypt from passlib.utils.compat import bytes, u, uascii_to_str, unicode import passlib.utils.handlers as uh @@ -144,9 +144,10 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. strict=strict and bool(chk), ) - def to_string(self, native=True): - hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum or u('')) - return uascii_to_str(hash) if native else hash + def to_string(self): + hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, + self.checksum or u('')) + return uascii_to_str(hash) #========================================================= # specialized salt generation - fixes passlib issue 25 @@ -228,18 +229,17 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. @classproperty def _has_backend_os_crypt(cls): - h1 = u('$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju') - h2 = u('$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty') - return bool(safe_os_crypt and safe_os_crypt(u("test"),h1)[1]==h1 and - safe_os_crypt(u("test"), h2)[1]==h2) + h1 = '$2$04$......................1O4gOrCYaqBG3o/4LnT2ykQUt1wbyju' + h2 = '$2a$04$......................qiOQjkB8hxU8OzRhS.GhRMa4VUnkPty' + return test_crypt("test",h1) and test_crypt("test", h2) @classmethod def _no_backends_msg(cls): return "no BCrypt backends available - please install pybcrypt or bcryptor for BCrypt support" def _calc_checksum_os_crypt(self, secret): - ok, hash = safe_os_crypt(secret, self.to_string(native=False)) - if ok: + hash = safe_crypt(secret, self.to_string()) + if hash: return hash[-31:] else: #NOTE: not checking backends since this is lowest priority, @@ -249,26 +249,22 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. def _calc_checksum_pybcrypt(self, secret): #pybcrypt behavior: - # py2: unicode secret -> ascii bytes (we override this) - # unicode hash -> ascii bytes (we provide ascii bytes) - # returns ascii bytes + # py2: unicode secret/hash encoded as ascii bytes before use, + # bytes takes as-is; returns ascii bytes. # py3: can't get to install if isinstance(secret, unicode): secret = secret.encode("utf-8") - hash = pybcrypt_hashpw(secret, - self.to_string(native=False)) + hash = pybcrypt_hashpw(secret, self.to_string()) return hash[-31:].decode("ascii") def _calc_checksum_bcryptor(self, secret): #bcryptor behavior: - # py2: unicode secret -> ascii bytes (we have to override) - # unicode hash -> ascii bytes (we provide ascii bytes) - # returns ascii bytes + # py2: unicode secret/hash encoded as ascii bytes before use, + # bytes takes as-is; returns ascii bytes. # py3: can't get to install if isinstance(secret, unicode): secret = secret.encode("utf-8") - hash = bcryptor_engine(False).hash_key(secret, - self.to_string(native=False)) + hash = bcryptor_engine(False).hash_key(secret, self.to_string()) return hash[-31:].decode("ascii") def _calc_checksum_builtin(self, secret): diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py index 976625a..b326e8e 100644 --- a/passlib/handlers/des_crypt.py +++ b/passlib/handlers/des_crypt.py @@ -58,7 +58,7 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import classproperty, h64, h64big, safe_os_crypt +from passlib.utils import classproperty, h64, h64big, safe_crypt, test_crypt from passlib.utils.compat import b, bytes, belem_ord, u, uascii_to_str, unicode from passlib.utils.des import mdes_encrypt_int_block import passlib.utils.handlers as uh @@ -193,9 +193,9 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): salt, chk = hash[:2], hash[2:] return cls(salt=salt, checksum=chk or None, strict=bool(chk)) - def to_string(self, native=True): + def to_string(self): hash = u("%s%s") % (self.salt, self.checksum or u('')) - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) #========================================================= #backend @@ -206,28 +206,23 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): @classproperty def _has_backend_os_crypt(cls): - h = u('abgOeLfPimXQo') - return bool(safe_os_crypt and safe_os_crypt(u("test"),h)[1]==h) + return test_crypt("test", 'abgOeLfPimXQo') def _calc_checksum_builtin(self, secret): - #gotta do something - no official policy since des-crypt predates unicode + # gotta do something - no official policy since des-crypt predates unicode if isinstance(secret, unicode): secret = secret.encode("utf-8") - #forbidding nul chars because linux crypt (and most C implementations) won't accept it either. + # forbidding nul chars because linux crypt (and most C implementations) + # won't accept it either. if b('\x00') in secret: raise ValueError("null char in secret") return raw_crypt(secret, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): - #os_crypt() would raise less useful error - null = u('\x00') if isinstance(secret, unicode) else b('\x00') - if null in secret: - raise ValueError("null char in secret") - - #NOTE: safe_os_crypt encodes unicode secret -> utf8 - #no official policy since des-crypt predates unicode - ok, hash = safe_os_crypt(secret, self.salt) - if ok: + # NOTE: safe_crypt encodes unicode secret -> utf8 + # no official policy since des-crypt predates unicode + hash = safe_crypt(secret, self.salt) + if hash: return hash[2:] else: return self._calc_checksum_builtin(secret) @@ -321,10 +316,10 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler strict=bool(chk), ) - def to_string(self, native=True): + def to_string(self): hash = u("_%s%s%s") % (h64.encode_int24(self.rounds).decode("ascii"), self.salt, self.checksum or u('')) - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) #========================================================= #backend @@ -335,8 +330,7 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler @classproperty def _has_backend_os_crypt(cls): - h = u('_/...lLDAxARksGCHin.') - return bool(safe_os_crypt and safe_os_crypt(u("test"),h)[1]==h) + return test_crypt("test", '_/...lLDAxARksGCHin.') def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): @@ -344,8 +338,8 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler return raw_ext_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): - ok, hash = safe_os_crypt(secret, self.to_string(native=False)) - if ok: + hash = safe_crypt(secret, self.to_string()) + if hash: return hash[9:] else: return self._calc_checksum_builtin(secret) @@ -414,9 +408,9 @@ class bigcrypt(uh.HasSalt, uh.GenericHandler): salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk, strict=bool(chk)) - def to_string(self, native=True): + def to_string(self): hash = u("%s%s") % (self.salt, self.checksum or u('')) - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) @classmethod def norm_checksum(cls, value, strict=False): @@ -499,9 +493,9 @@ class crypt16(uh.HasSalt, uh.GenericHandler): salt, chk = m.group("salt", "chk") return cls(salt=salt, checksum=chk, strict=bool(chk)) - def to_string(self, native=True): + def to_string(self): hash = u("%s%s") % (self.salt, self.checksum or u('')) - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) #========================================================= #backend diff --git a/passlib/handlers/md5_crypt.py b/passlib/handlers/md5_crypt.py index 2dd9e7e..e24e66e 100644 --- a/passlib/handlers/md5_crypt.py +++ b/passlib/handlers/md5_crypt.py @@ -9,7 +9,7 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import classproperty, h64, safe_os_crypt +from passlib.utils import classproperty, h64, safe_crypt, test_crypt from passlib.utils.compat import b, bytes, irange, unicode, u import passlib.utils.handlers as uh #pkg @@ -227,15 +227,14 @@ class md5_crypt(uh.HasManyBackends, _Md5Common): @classproperty def _has_backend_os_crypt(cls): - h = u('$1$test$pi/xDtU5WFVRqYS6BMU8X/') - return bool(safe_os_crypt and safe_os_crypt(u("test"),h)[1]==h) + return test_crypt("test", '$1$test$pi/xDtU5WFVRqYS6BMU8X/') def _calc_checksum_builtin(self, secret): return raw_md5_crypt(secret, self.salt) def _calc_checksum_os_crypt(self, secret): - ok, hash = safe_os_crypt(secret, self.ident + self.salt) - if ok: + hash = safe_crypt(secret, self.ident + self.salt) + if hash: return hash[-22:] else: return self._calc_checksum_builtin(secret) diff --git a/passlib/handlers/pbkdf2.py b/passlib/handlers/pbkdf2.py index 748d75d..8621f03 100644 --- a/passlib/handlers/pbkdf2.py +++ b/passlib/handlers/pbkdf2.py @@ -11,7 +11,7 @@ from warnings import warn #site #libs from passlib.utils import ab64_decode, ab64_encode -from passlib.utils.compat import b, bytes, u, uascii_to_str, unicode +from passlib.utils.compat import b, bytes, str_to_bascii, u, uascii_to_str, unicode from passlib.utils.pbkdf2 import pbkdf2 import passlib.utils.handlers as uh #pkg @@ -313,14 +313,14 @@ class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler): strict=bool(chk), ) - def to_string(self, withchk=True, native=True): + def to_string(self, withchk=True): if self.rounds == 400: hash = u('$p5k2$$%s') % (self.salt,) else: hash = u('$p5k2$%x$%s') % (self.rounds, self.salt) if withchk and self.checksum: hash = u("%s$%s") % (hash,self.checksum) - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) #========================================================= #backend @@ -328,7 +328,7 @@ class dlitz_pbkdf2_sha1(uh.HasRounds, uh.HasSalt, uh.GenericHandler): def calc_checksum(self, secret): if isinstance(secret, unicode): secret = secret.encode("utf-8") - salt = self.to_string(withchk=False, native=False).encode("ascii") + salt = str_to_bascii(self.to_string(withchk=False)) result = pbkdf2(secret, salt, self.rounds, 24, "hmac-sha1") return ab64_encode(result).decode("ascii") diff --git a/passlib/handlers/sha1_crypt.py b/passlib/handlers/sha1_crypt.py index c2eb41d..2a12345 100644 --- a/passlib/handlers/sha1_crypt.py +++ b/passlib/handlers/sha1_crypt.py @@ -13,7 +13,7 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import classproperty, h64, safe_os_crypt +from passlib.utils import classproperty, h64, safe_crypt, test_crypt from passlib.utils.compat import b, bytes, u, uascii_to_str, unicode from passlib.utils.pbkdf2 import hmac_sha1 import passlib.utils.handlers as uh @@ -90,11 +90,11 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler strict=bool(chk), ) - def to_string(self, native=True): + def to_string(self): hash = u("$sha1$%d$%s") % (self.rounds, self.salt) if self.checksum: hash += u("$") + self.checksum - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) #========================================================= #backend @@ -105,8 +105,8 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler @classproperty def _has_backend_os_crypt(cls): - h = u('$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHimExLaiSFlGkAe') - return bool(safe_os_crypt and safe_os_crypt(u("test"),h)[1]==h) + return test_crypt("test", '$sha1$1$Wq3GL2Vp$C8U''25GvfHS8qGHim' + 'ExLaiSFlGkAe') def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): @@ -132,8 +132,8 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler ] def _calc_checksum_os_crypt(self, secret): - ok, hash = safe_os_crypt(secret, self.to_string(native=False)) - if ok: + hash = safe_crypt(secret, self.to_string()) + if hash: return hash[hash.rindex("$")+1:] else: return self._calc_checksum_builtin(secret) diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py index 8da3f1a..58f32a6 100644 --- a/passlib/handlers/sha2_crypt.py +++ b/passlib/handlers/sha2_crypt.py @@ -9,7 +9,7 @@ import logging; log = logging.getLogger(__name__) from warnings import warn #site #libs -from passlib.utils import classproperty, h64, safe_os_crypt +from passlib.utils import classproperty, h64, safe_crypt, test_crypt from passlib.utils.compat import b, bytes, belem_ord, irange, u, \ uascii_to_str, unicode import passlib.utils.handlers as uh @@ -314,12 +314,12 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl strict=bool(chk), ) - def to_string(self, native=True): + def to_string(self): if self.rounds == 5000 and self.implicit_rounds: hash = u("$5$%s$%s") % (self.salt, self.checksum or u('')) else: hash = u("$5$rounds=%d$%s$%s") % (self.rounds, self.salt, self.checksum or u('')) - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) #========================================================= #backend @@ -330,8 +330,8 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl @classproperty def _has_backend_os_crypt(cls): - h = u("$5$rounds=1000$test$QmQADEXMG8POI5WDsaeho0P36yK3Tcrgboabng6bkb/") - return bool(safe_os_crypt and safe_os_crypt(u("test"),h)[1]==h) + return test_crypt("test", "$5$rounds=1000$test$QmQADEXMG8POI5W" + "Dsaeho0P36yK3Tcrgboabng6bkb/") def _calc_checksum_builtin(self, secret): if isinstance(secret, unicode): @@ -346,12 +346,12 @@ class sha256_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl return checksum.decode("ascii") def _calc_checksum_os_crypt(self, secret): - ok, result = safe_os_crypt(secret, self.to_string(native=False)) - if ok: + hash = safe_crypt(secret, self.to_string()) + if hash: #NOTE: avoiding full parsing routine via from_string().checksum, # and just extracting the bit we need. - assert result.startswith(u("$5$")) - chk = result[-43:] + assert hash.startswith(u("$5$")) + chk = hash[-43:] assert u('$') not in chk return chk else: @@ -466,12 +466,12 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl strict=bool(chk), ) - def to_string(self, native=True): + def to_string(self): if self.rounds == 5000 and self.implicit_rounds: hash = u("$6$%s$%s") % (self.salt, self.checksum or u('')) else: hash = u("$6$rounds=%d$%s$%s") % (self.rounds, self.salt, self.checksum or u('')) - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) #========================================================= #backend @@ -482,8 +482,10 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl @classproperty def _has_backend_os_crypt(cls): - h = u("$6$rounds=1000$test$2M/Lx6MtobqjLjobw0Wmo4Q5OFx5nVLJvmgseatA6oMnyWeBdRDx4DU.1H3eGmse6pgsOgDisWBGI5c7TZauS0") - return bool(safe_os_crypt and safe_os_crypt(u("test"),h)[1]==h) + return test_crypt("test", "$6$rounds=1000$test$2M/Lx6Mtobqj" + "Ljobw0Wmo4Q5OFx5nVLJvmgseatA6oMn" + "yWeBdRDx4DU.1H3eGmse6pgsOgDisWBG" + "I5c7TZauS0") #NOTE: testing w/ HashTimer shows 64-bit linux's crypt to be ~2.6x faster than builtin (627253 vs 238152 rounds/sec) @@ -500,12 +502,12 @@ class sha512_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandl return checksum.decode("ascii") def _calc_checksum_os_crypt(self, secret): - ok, result = safe_os_crypt(secret, self.to_string(native=False)) - if ok: + hash = safe_crypt(secret, self.to_string()) + if hash: #NOTE: avoiding full parsing routine via from_string().checksum, # and just extracting the bit we need. - assert result.startswith(u("$6$")) - chk = result[-86:] + assert hash.startswith(u("$6$")) + chk = hash[-86:] assert u('$') not in chk return chk else: diff --git a/passlib/handlers/sun_md5_crypt.py b/passlib/handlers/sun_md5_crypt.py index c7b99bb..1a93c41 100644 --- a/passlib/handlers/sun_md5_crypt.py +++ b/passlib/handlers/sun_md5_crypt.py @@ -19,7 +19,7 @@ from warnings import warn #libs from passlib.utils import h64 from passlib.utils.compat import b, bytes, belem_ord, trange, u, \ - uascii_to_str, unicode + uascii_to_str, unicode, str_to_bascii import passlib.utils.handlers as uh #pkg #local @@ -307,7 +307,7 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): strict=bool(chk), ) - def to_string(self, withchk=True, native=True): + def to_string(self, withchk=True): ss = u('') if self.bare_salt else u('$') rounds = self.rounds if rounds > 0: @@ -318,7 +318,7 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): chk = self.checksum if chk: hash = u("%s$%s") % (hash, chk) - return uascii_to_str(hash) if native else hash + return uascii_to_str(hash) #========================================================= #primary interface @@ -334,7 +334,7 @@ class sun_md5_crypt(uh.HasRounds, uh.HasSalt, uh.GenericHandler): raise TypeError("no secret specified") if isinstance(secret, unicode): secret = secret.encode("utf-8") - config = self.to_string(withchk=False,native=False).encode("ascii") + config = str_to_bascii(self.to_string(withchk=False)) return raw_sun_md5_crypt(secret, self.rounds, config).decode("ascii") #========================================================= diff --git a/passlib/hosts.py b/passlib/hosts.py index a88e599..3533dad 100644 --- a/passlib/hosts.py +++ b/passlib/hosts.py @@ -9,7 +9,7 @@ from warnings import warn from passlib.context import LazyCryptContext from passlib.exc import PasslibRuntimeWarning from passlib.registry import get_crypt_handler -from passlib.utils import has_os_crypt, unix_crypt_schemes +from passlib.utils import has_crypt, unix_crypt_schemes #local __all__ = [ "linux_context", "linux2_context", @@ -57,7 +57,7 @@ netbsd_context = LazyCryptContext([ "bcrypt", "sha1_crypt", "md5_crypt", "bsdi_c #========================================================= #current host #========================================================= -if has_os_crypt: +if has_crypt: #NOTE: this is basically mimicing the output of os crypt(), #except that it uses passlib's (usually stronger) defaults settings, #and can be introspected and used much more flexibly. diff --git a/passlib/tests/test_utils.py b/passlib/tests/test_utils.py index d0609b7..957ceb4 100644 --- a/passlib/tests/test_utils.py +++ b/passlib/tests/test_utils.py @@ -98,54 +98,58 @@ class MiscTest(TestCase): rng.seed(genseed(rng)) - def test_safe_os_crypt(self): - "test safe_os_crypt() wrapper" - from passlib.utils import safe_os_crypt - - if not safe_os_crypt: - raise self.skipTest("stdlib crypt module not available") - - #NOTE: this is assuming EVERY crypt will support des_crypt. - # if this fails on some platform, this test will need modifying. - - #test normal case - ok, hash = safe_os_crypt(u('test'), u('aa')) - self.assertTrue(ok) - self.assertIsInstance(hash, unicode) - self.assertEqual(hash, u('aaqPiZY5xR5l.')) - - #test hash-as-bytes - self.assertRaises(TypeError, safe_os_crypt, u('test'), b('aa')) - - #test password as ascii - ret = safe_os_crypt(b('test'), u('aa')) - self.assertEqual(ret, (True, u('aaqPiZY5xR5l.'))) - - #test unicode password w/ high char - ret = safe_os_crypt(u('test\u1234'), u('aa')) - self.assertEqual(ret, (True, u('aahWwbrUsKZk.'))) - - #test utf-8 password w/ high char - ret = safe_os_crypt(b('test\xe1\x88\xb4'), u('aa')) - self.assertEqual(ret, (True, u('aahWwbrUsKZk.'))) - - #test latin-1 password - ret = safe_os_crypt(b('test\xff'), u('aa')) - if PY3: - self.assertEqual(ret, (False, None)) - else: - self.assertEqual(ret, (True, u('aaOx.5nbTU/.M'))) - - # test safe_os_crypt() handles os_crypt() returning None + def test_crypt(self): + "test crypt.crypt() wrappers" + from passlib.utils import has_crypt, safe_crypt, test_crypt + + # test everything is disabled + if not has_crypt: + self.assertEqual(safe_crypt("test", "aa"), None) + self.assertFalse(test_crypt("test", "aaqPiZY5xR5l.")) + raise self.skipTest("crypt.crypt() not available") + + # XXX: this assumes *every* crypt() implementation supports des_crypt. + # if this fails for some platform, this test will need modifying. + + # test return type + self.assertIsInstance(safe_crypt(u("test"), u("aa")), unicode) + + # test ascii password + h1 = u('aaqPiZY5xR5l.') + self.assertEqual(safe_crypt(u('test'), u('aa')), h1) + self.assertEqual(safe_crypt(b('test'), b('aa')), h1) + + # test utf-8 / unicode password + h2 = u('aahWwbrUsKZk.') + self.assertEqual(safe_crypt(u('test\u1234'), 'aa'), h2) + self.assertEqual(safe_crypt(b('test\xe1\x88\xb4'), 'aa'), h2) + + # test latin-1 password + hash = safe_crypt(b('test\xff'), 'aa') + if PY3: # py3 supports utf-8 bytes only. + self.assertEqual(hash, None) + else: # but py2 is fine. + self.assertEqual(hash, u('aaOx.5nbTU/.M')) + + # test rejects null chars in password + self.assertRaises(ValueError, safe_crypt, '\x00', 'aa') + + # check test_crypt() + h1x = h1[:-1] + 'x' + self.assertTrue(test_crypt("test", h1)) + self.assertFalse(test_crypt("test", h1x)) + + # test crypt.crypt() returning None is supported. # (Python's Modules/_cryptmodule.c notes some platforms may do this - # when algorithm is not supported) + # when algorithm is not supported - but don't say which platforms) import passlib.utils as mod - orig = mod.os_crypt + orig = mod._crypt try: - mod.os_crypt = lambda secret, hash: None - self.assertEqual(safe_os_crypt(u('test'), u('aa')), (False,None)) + mod._crypt = lambda secret, hash: None + self.assertEqual(safe_crypt("test", "aa"), None) + self.assertFalse(test_crypt("test", h1)) finally: - mod.os_crypt = orig + mod._crypt = orig def test_consteq(self): "test consteq()" diff --git a/passlib/tests/test_utils_handlers.py b/passlib/tests/test_utils_handlers.py index 630e1a0..fe2667a 100644 --- a/passlib/tests/test_utils_handlers.py +++ b/passlib/tests/test_utils_handlers.py @@ -344,7 +344,10 @@ class PrefixWrapperTest(TestCase): d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}") self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds) - self.assertTrue('max_rounds' in dir(d2)) + if PY_25_MAX: # lacks __dir__() support + self.assertFalse('max_rounds' in dir(d2)) + else: + self.assertTrue('max_rounds' in dir(d2)) def test_11_wrapped_methods(self): d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}") diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index fc53edc..628c4ee 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -10,7 +10,7 @@ import re import os import sys import tempfile -from passlib.utils.compat import PY27, PY_MIN_32, PY3 +from passlib.utils.compat import PY2, PY27, PY_MIN_32, PY3 try: import unittest2 as unittest @@ -452,11 +452,6 @@ class HandlerCase(TestCase): #: if handler uses multiple backends, explicitly set this one when running tests. backend = None - #: hack used by create_backend() to signal we should monkeypatch - # safe_os_crypt() to use handler+this backend, - # only used when backend == "os_crypt" - _patch_crypt_backend = None - #========================================================= #alg interface helpers - allows subclass to overide how # default tests invoke the handler (eg for context_kwds) @@ -526,7 +521,7 @@ class HandlerCase(TestCase): #setup / cleanup #========================================================= _orig_backend = None #backup of original backend - _orig_os_crypt = None #backup of original utils.os_crypt + _orig_crypt = None #backup of original utils.os_crypt def setUp(self): h = self.handler @@ -539,12 +534,12 @@ class HandlerCase(TestCase): if (backend == "os_crypt" and not h.has_backend("os_crypt")): alt_backend = _has_other_backends(h, "os_crypt") if alt_backend: - #monkeypatch utils.safe_os_crypt to use specific handler+backend - #this allows use to test as much of the hash's code path + #monkeypatch utils.safe_crypt to use specific handler+backend + #this allows us to test as much of the hash's code path #as possible, even if current OS doesn't provide crypt() support #for the hash. import passlib.utils as mod - self._orig_os_crypt = mod.os_crypt + self._orig_crypt = mod._crypt def crypt_stub(secret, hash): tmp = h.get_backend() try: @@ -552,16 +547,15 @@ class HandlerCase(TestCase): hash = h.genhash(secret, hash) finally: h.set_backend(tmp) - if not PY3 and isinstance(hash, unicode): - hash = hash.encode("ascii") + assert isinstance(hash, str) return hash - mod.os_crypt = crypt_stub + mod._crypt = crypt_stub h.set_backend(backend) def tearDown(self): - if self._orig_os_crypt: + if self._orig_crypt: import passlib.utils as mod - mod.os_crypt = self._orig_os_crypt + mod._crypt = self._orig_crypt if self._orig_backend: self.handler.set_backend(self._orig_backend) @@ -1003,16 +997,16 @@ class HandlerCase(TestCase): else: helpers = [] - # provide default "os_crypt" helper - if hasattr(handler, "has_backend") and \ - 'os_crypt' in handler.backends and \ - not hasattr(handler, "orig_prefix"): + # use crypt.crypt() to check handlers that have an 'os_crypt' backend. + if _has_possible_crypt_support(handler): possible = True - if handler.has_backend("os_crypt"): + # NOTE: disabling when self._orig_crypt set, means has_backend + # will return a false positive. + if not self._orig_crypt and handler.has_backend("os_crypt"): def check_crypt(secret, hash): - from passlib.utils import os_crypt - self.assertEqual(os_crypt(secret, hash), hash, - "os_crypt(%r,%r):" % (secret, hash)) + from crypt import crypt + self.assertEqual(crypt(secret, hash), hash, + "crypt.crypt(%r,%r):" % (secret, hash)) helpers.append(check_crypt) if not helpers: @@ -1022,7 +1016,7 @@ class HandlerCase(TestCase): raise self.skipTest("not applicable") # generate a single hash, and verify it using all helpers. - secret = 't\xc3\xa1\xd0\x91\xe2\x84\x93\xc9\x99' + secret = b('t\xc3\xa1\xd0\x91\xe2\x84\x93\xc9\x99').decode("utf-8") hash = self.do_encrypt(secret) for helper in helpers: helper(secret, hash) @@ -1081,8 +1075,8 @@ def _enable_backend_case(handler, backend): if enable_option("all-backends") or _is_default_backend(handler, backend): if handler.has_backend(backend): return True, None - from passlib.utils import has_os_crypt - if backend == "os_crypt" and has_os_crypt: + from passlib.utils import has_crypt + if backend == "os_crypt" and has_crypt: if enable_option("cover") and _has_other_backends(handler, "os_crypt"): #in this case, HandlerCase will monkeypatch os_crypt #to use another backend, just so we can test os_crypt fully. @@ -1112,6 +1106,12 @@ def _has_other_backends(handler, ignore): return name return None +def _has_possible_crypt_support(handler): + "check if crypt() supports this natively on some platforms" + return hasattr(handler, "backends") and \ + 'os_crypt' in handler.backends and \ + not hasattr(handler, "orig_prefix") # ignore wrapper classes + def create_backend_case(base, name, module="passlib.tests.test_handlers"): "create a test case for specific backend of a multi-backend handler" #get handler, figure out if backend should be tested diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py index c0161f0..b570480 100644 --- a/passlib/utils/__init__.py +++ b/passlib/utils/__init__.py @@ -56,7 +56,9 @@ __all__ = [ "ab64_encode", "ab64_decode", # host OS - 'os_crypt', + 'has_crypt', + 'test_crypt', + 'safe_crypt', 'tick', # randomness @@ -83,7 +85,7 @@ JYTHON = sys.platform.startswith('java') # bitsize of system architecture (32 or 64) sys_bits = int(math.log(sys.maxsize if PY3 else sys.maxint, 2) + 1.5) -# list of hashes supported by os.crypt() on at least one OS. +# list of hashes algs supported by crypt() on at least one OS. unix_crypt_schemes = [ "sha512_crypt", "sha256_crypt", "sha1_crypt", "bcrypt", @@ -1126,26 +1128,28 @@ def ab64_decode(data): # host OS helpers #================================================================================= -#expose crypt function as 'os_crypt', set to None if not available. try: - from crypt import crypt as os_crypt + from crypt import crypt as _crypt except ImportError: #pragma: no cover - safe_os_crypt = os_crypt = None - has_os_crypt = False + has_crypt = False + def safe_crypt(secret, hash): + return None else: - # NOTE: see docstring below as to why we're wrapping os_crypt() - has_os_crypt = True + has_crypt = True + _NULL = '\x00' if PY3: - def safe_os_crypt(secret, hash): + def safe_crypt(secret, hash): if isinstance(secret, bytes): - # decode secret using utf-8, and make sure it re-encodes to - # match the original - otherwise the call to os_crypt() - # will encode the wrong password. + # Python 3's crypt() only accepts unicode, which is then + # encoding using utf-8 before passing to the C-level crypt(). + # so we have to decode the secret, but also check that it + # re-encodes to the original sequence of bytes... otherwise + # the call to crypt() will digest the wrong value. orig = secret try: secret = secret.decode("utf-8") except UnicodeDecodeError: - return False, None + return None if secret.encode("utf-8") != orig: # just in case original encoding wouldn't be reproduced # during call to os_crypt. not sure if/how this could @@ -1153,46 +1157,62 @@ else: from passlib.exc import PasslibRuntimeWarning warn("utf-8 password didn't re-encode correctly!", PasslibRuntimeWarning) - return False, None - result = os_crypt(secret, hash) - return (result is not None), result + return None + if _NULL in secret: + raise ValueError("null character in secret") + if isinstance(hash, bytes): + hash = hash.decode("ascii") + # NOTE: may return None on some OSes, if hash not supported. + return _crypt(secret, hash) else: - def safe_os_crypt(secret, hash): - # NOTE: this guard logic is designed purely to match py3 behavior, - # with the exception that it accepts secret as bytes. + def safe_crypt(secret, hash): if isinstance(secret, unicode): secret = secret.encode("utf-8") - if isinstance(hash, bytes): - raise TypeError("hash must be unicode") - else: - hash = hash.encode("utf-8") - result = os_crypt(secret, hash) + if _NULL in secret: + raise ValueError("null character in secret") + if isinstance(hash, unicode): + hash = hash.encode("ascii") + # NOTE: may return None on some OSes, if hash not supported. + result = _crypt(secret, hash) if result is None: - return False, None + return None else: - return True, result.decode("ascii") + return result.decode("ascii") - _add_doc(safe_os_crypt, """wrapper around stdlib's crypt. +_add_doc(safe_crypt, """wrapper around stdlib's crypt. - Python 3's crypt behaves slightly differently from Python 2's crypt. - for one, it takes in and returns unicode. - internally, it converts to utf-8 before hashing. - Annoyingly, *there is no way to call it using bytes*. - thus, it can't be used to hash non-ascii passwords - using any encoding but utf-8 (eg, using latin-1). + This is a wrapper around stdlib's :func:`!crypt.crypt`, which attempts + to provide uniform behavior across Python 2 and 3. - This wrapper attempts to gloss over all those issues: - Under Python 2, it accept passwords as unicode or bytes, - accepts hashes only as unicode, and always returns unicode. - Under Python 3, it will signal that it cannot hash a password - if provided as non-utf-8 bytes, but otherwise behave the same as crypt. + :arg secret: + password, as bytes or unicode (unicode will be encoded as ``utf-8``). - :arg secret: password as bytes or unicode - :arg hash: hash/salt as unicode - :returns: - ``(False, None)`` if the password can't be hashed (3.x only), - or ``(True, result: unicode)`` otherwise. - """) + :arg hash: + hash or config string, as ascii bytes or unicode. + + :returns: + resulting hash as ascii unicode; or ``None`` if the password + couldn't be hashed due to one of the issues: + + * :func:`crypt()` not available on platform. + + * Under Python 3, if *secret* is specified as bytes, + it must be use ``utf-8`` or it can't be passed + to :func:`crypt()`. + + * Some OSes will return ``None`` if they don't recognize + the algorithm being used (though most will simply fall + back to des-crypt). + """) + +def test_crypt(secret, hash): + """check if :func:`crypt.crypt` supports specific hash + :arg secret: password to test + :arg hash: known hash of password to use as reference + :returns: True or False + """ + assert secret and hash + return safe_crypt(secret, hash) == hash # pick best timer function to expose as "tick" - lifted from timeit module. if sys.platform == "win32": diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py index 6e2763b..4f4a54f 100644 --- a/passlib/utils/handlers.py +++ b/passlib/utils/handlers.py @@ -435,7 +435,6 @@ class GenericHandler(object): #NOTE: documenting some non-standardized but common kwd flags # that passlib to_string() method may have # - # native=True -- if false, return unicode under py2 -- ignored under py3 # withchk=True -- if false, omit checksum portion of hash # raise NotImplementedError("%s must implement from_string()" % (type(self),)) |
