diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2012-01-19 00:13:49 -0500 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2012-01-19 00:13:49 -0500 |
| commit | bcf652e665ba2ffbf8e3f43b4e1108e7aa29dbb0 (patch) | |
| tree | 3a2aff7abc0d80d9c560846622f89439634f25ce /passlib/utils | |
| parent | 6f816a3d7f1f3a394fedcc0aa410792a95b9ece6 (diff) | |
| download | passlib-bcf652e665ba2ffbf8e3f43b4e1108e7aa29dbb0.tar.gz | |
simplified crypt.crypt() wrappers
* safe_crypt() improved - accepts unicode/bytes for salt, checks for NULL, returns None on failure
* added test_crypt() wrapper to simplify backend checks.
* removed native=True from most to_string() implementations, unused now.
* updated UTs
Diffstat (limited to 'passlib/utils')
| -rw-r--r-- | passlib/utils/__init__.py | 108 | ||||
| -rw-r--r-- | passlib/utils/handlers.py | 1 |
2 files changed, 64 insertions, 45 deletions
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),)) |
