summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/lib/passlib.utils.rst2
-rw-r--r--passlib/handlers/bcrypt.py38
-rw-r--r--passlib/handlers/des_crypt.py46
-rw-r--r--passlib/handlers/md5_crypt.py9
-rw-r--r--passlib/handlers/pbkdf2.py8
-rw-r--r--passlib/handlers/sha1_crypt.py14
-rw-r--r--passlib/handlers/sha2_crypt.py36
-rw-r--r--passlib/handlers/sun_md5_crypt.py8
-rw-r--r--passlib/hosts.py4
-rw-r--r--passlib/tests/test_utils.py92
-rw-r--r--passlib/tests/test_utils_handlers.py5
-rw-r--r--passlib/tests/utils.py52
-rw-r--r--passlib/utils/__init__.py108
-rw-r--r--passlib/utils/handlers.py1
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),))