diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2012-01-18 16:28:38 -0500 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2012-01-18 16:28:38 -0500 |
| commit | ffb343b6bb0ba8b6015f19493347c7aae65739fd (patch) | |
| tree | ec69a3f9f34296bc4348526f410b25239fa6d432 | |
| parent | 1f601a7f8c621d36504c400f091e23caf0dfdebd (diff) | |
| download | passlib-ffb343b6bb0ba8b6015f19493347c7aae65739fd.tar.gz | |
reordered contents of utils
| -rw-r--r-- | passlib/utils/__init__.py | 535 |
1 files changed, 264 insertions, 271 deletions
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py index 63b87af..6a6409b 100644 --- a/passlib/utils/__init__.py +++ b/passlib/utils/__init__.py @@ -35,20 +35,6 @@ __all__ = [ #byte compat aliases 'bytes', - #misc - 'os_crypt', - - # object type / interface tests - 'is_crypt_handler', - 'is_crypt_context', - 'has_rounds_info', - 'has_salt_info', - - #bytes<->unicode - 'to_bytes', - 'to_unicode', - 'is_same_codec', - # string manipulation 'consteq', 'saslprep', @@ -56,12 +42,18 @@ __all__ = [ #byte manipulation "xor_bytes", + #bytes<->unicode + 'to_bytes', + 'to_unicode', + 'is_same_codec', + # base64 helpers "BASE64_CHARS", "HASH64_CHARS", "BCRYPT_CHARS", "AB64_CHARS", "Base64Engine", "h64", "h64big", "ab64_encode", "ab64_decode", # host OS + 'os_crypt', 'tick', # randomness @@ -69,10 +61,16 @@ __all__ = [ 'getrandbytes', 'getrandstr', 'generate_password', + + # object type / interface tests + 'is_crypt_handler', + 'is_crypt_context', + 'has_rounds_info', + 'has_salt_info', ] #================================================================================= -#constants +# constants #================================================================================= # list of hashes supported by os.crypt() on at least one OS. @@ -118,74 +116,6 @@ _UEMPTY = u("") _USPACE = u(" ") #================================================================================= -#os crypt helpers -#================================================================================= - -#expose crypt function as 'os_crypt', set to None if not available. -try: - from crypt import crypt as os_crypt -except ImportError: #pragma: no cover - safe_os_crypt = os_crypt = None -else: - # NOTE: see docstring below as to why we're wrapping os_crypt() - if PY3: - def safe_os_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. - orig = secret - try: - secret = secret.decode("utf-8") - except UnicodeDecodeError: - return False, 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 - # happen, but being paranoid. - warn("utf-8 password didn't re-encode correctly!") - return False, None - result = os_crypt(secret, hash) - return (result is not None), result - 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. - 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 result is None: - return False, None - else: - return True, result.decode("ascii") - - _add_doc(safe_os_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 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 - :arg hash: hash/salt as unicode - :returns: - ``(False, None)`` if the password can't be hashed (3.x only), - or ``(True, result: unicode)`` otherwise. - """) - -#================================================================================= #decorators and meta helpers #================================================================================= class classproperty(object): @@ -301,188 +231,6 @@ def relocated_function(target, msg=None, name=None, deprecated=None, mod=None, ## def __func__(self): ## "py3 compatible alias" -#========================================================== -#protocol helpers -#========================================================== -_handler_attrs = ( - "name", - "setting_kwds", "context_kwds", - "genconfig", "genhash", - "verify", "encrypt", "identify", - ) - -def is_crypt_handler(obj): - "check if object follows the :ref:`password-hash-api`" - return all(hasattr(obj, name) for name in _handler_attrs) - -_context_attrs = ( - "hash_needs_update", - "genconfig", "genhash", - "verify", "encrypt", "identify", - ) - -def is_crypt_context(obj): - "check if object appears to be a :class:`~passlib.context.CryptContext` instance" - return all(hasattr(obj, name) for name in _context_attrs) - -##def has_many_backends(handler): -## "check if handler provides multiple baceknds" -## #NOTE: should also provide get_backend(), .has_backend(), and .backends attr -## return hasattr(handler, "set_backend") - -def has_rounds_info(handler): - "check if handler provides the optional :ref:`rounds information <optional-rounds-attributes>` attributes" - return 'rounds' in handler.setting_kwds and getattr(handler, "min_rounds", None) is not None - -def has_salt_info(handler): - "check if handler provides the optional :ref:`salt information <optional-salt-attributes>` attributes" - return 'salt' in handler.setting_kwds and getattr(handler, "min_salt_size", None) is not None - -##def has_raw_salt(handler): -## "check if handler takes in encoded salt as unicode (False), or decoded salt as bytes (True)" -## sc = getattr(handler, "salt_chars", None) -## if sc is None: -## return None -## elif isinstance(sc, unicode): -## return False -## elif isinstance(sc, bytes): -## return True -## else: -## raise TypeError("handler.salt_chars must be None/unicode/bytes") - -#========================================================== -#bytes <-> unicode conversion helpers -#========================================================== - -def to_bytes(source, encoding="utf-8", source_encoding=None, errname="value"): - """helper to encoding unicode -> bytes - - this function takes in a ``source`` string. - if unicode, encodes it using the specified ``encoding``. - if bytes, returns unchanged - unless ``source_encoding`` - is specified, in which case the bytes are transcoded - if and only if the source encoding doesn't match - the desired encoding. - all other types result in a :exc:`TypeError`. - - :arg source: source bytes/unicode to process - :arg encoding: target character encoding or ``None``. - :param source_encoding: optional source encoding - :param errname: optional name of variable/noun to reference when raising errors - - :raises TypeError: if unicode encountered but ``encoding=None`` specified; - or if source is not unicode or bytes. - - :returns: bytes object - - .. note:: - - if ``encoding`` is set to ``None``, then unicode strings - will be rejected, and only byte strings will be allowed through. - """ - if isinstance(source, bytes): - if source_encoding and encoding and \ - not is_same_codec(source_encoding, encoding): - return source.decode(source_encoding).encode(encoding) - else: - return source - elif not encoding: - raise TypeError("%s must be bytes, not %s" % (errname, type(source))) - elif isinstance(source, unicode): - return source.encode(encoding) - elif source_encoding: - raise TypeError("%s must be unicode or %s-encoded bytes, not %s" % - (errname, source_encoding, type(source))) - else: - raise TypeError("%s must be unicode or bytes, not %s" % (errname, type(source))) - -def to_unicode(source, source_encoding="utf-8", errname="value"): - """take in unicode or bytes, return unicode - - if bytes provided, decodes using specified encoding. - leaves unicode alone. - - :raises TypeError: if source is not unicode or bytes. - - :arg source: source bytes/unicode to process - :arg source_encoding: encoding to use when decoding bytes instances - :param errname: optional name of variable/noun to reference when raising errors - - :returns: unicode object - """ - if isinstance(source, unicode): - return source - elif not source_encoding: - raise TypeError("%s must be unicode, not %s" % (errname, type(source))) - elif isinstance(source, bytes): - return source.decode(source_encoding) - else: - raise TypeError("%s must be unicode or %s-encoded bytes, not %s" % - (errname, source_encoding, type(source))) - -if PY3: - def to_native_str(source, encoding="utf-8", errname="value"): - if isinstance(source, bytes): - return source.decode(encoding) - elif isinstance(source, unicode): - return source - else: - raise TypeError("%s must be unicode or bytes, not %s" % - (errname, type(source))) -else: - def to_native_str(source, encoding="utf-8", errname="value"): - if isinstance(source, bytes): - return source - elif isinstance(source, unicode): - return source.encode(encoding) - else: - raise TypeError("%s must be unicode or bytes, not %s" % - (errname, type(source))) - -_add_doc(to_native_str, - """take in unicode or bytes, return native string - - python 2: encodes unicode using specified encoding, leaves bytes alone. - python 3: decodes bytes using specified encoding, leaves unicode alone. - - :raises TypeError: if source is not unicode or bytes. - - :arg source: - source unicode or bytes string. - - :arg encoding: - encoding to use when encoding unicode or decoding bytes. - this defaults to ``"utf-8"``. - - :param errname: - optional name of variable/noun to reference when raising errors. - - :returns: :class:`str` instance - """) - -@deprecated_function(deprecated="1.6", removed="1.7") -def to_hash_str(source, encoding="ascii"): - "deprecated, use to_native_str() instead" - return to_native_str(source, encoding, 'hash') - -#-------------------------------------------------- -#support utils -#-------------------------------------------------- -def is_same_codec(left, right): - "check if two codecs names are aliases for same codec" - if left == right: - return True - if not (left and right): - return False - return _lookup_codec(left).name == _lookup_codec(right).name - -_B80 = 128 if PY3 else b('\x80') -_U80 = u('\x80') -def is_ascii_safe(source): - "check if source (bytes or unicode) contains only 7-bit ascii" - r = _B80 if isinstance(source, bytes) else _U80 - return all(c < r for c in source) - #================================================================================= #string helpers #================================================================================= @@ -733,6 +481,15 @@ else: def bjoin_ints(values): return bjoin(chr(v) for v in values) +if PY3: + def xor_bytes(left, right): + "perform bitwise-xor of two byte-strings" + return bytes(l ^ r for l, r in zip(left, right)) +else: + def xor_bytes(left, right): + "perform bitwise-xor of two byte-strings" + return bjoin(chr(ord(l) ^ ord(r)) for l, r in zip(left, right)) + def render_bytes(source, *args): """helper for using formatting operator with bytes. @@ -776,14 +533,135 @@ def int_to_bytes(value, count): for s in irange(8*count-8,-8,-8) ) +#========================================================== +# bytes <-> unicode conversion helpers +#========================================================== + +def is_same_codec(left, right): + "check if two codecs names are aliases for same codec" + if left == right: + return True + if not (left and right): + return False + return _lookup_codec(left).name == _lookup_codec(right).name + +_B80 = 128 if PY3 else b('\x80') +_U80 = u('\x80') +def is_ascii_safe(source): + "check if source (bytes or unicode) contains only 7-bit ascii" + r = _B80 if isinstance(source, bytes) else _U80 + return all(c < r for c in source) + +def to_bytes(source, encoding="utf-8", source_encoding=None, errname="value"): + """helper to encoding unicode -> bytes + + this function takes in a ``source`` string. + if unicode, encodes it using the specified ``encoding``. + if bytes, returns unchanged - unless ``source_encoding`` + is specified, in which case the bytes are transcoded + if and only if the source encoding doesn't match + the desired encoding. + all other types result in a :exc:`TypeError`. + + :arg source: source bytes/unicode to process + :arg encoding: target character encoding or ``None``. + :param source_encoding: optional source encoding + :param errname: optional name of variable/noun to reference when raising errors + + :raises TypeError: if unicode encountered but ``encoding=None`` specified; + or if source is not unicode or bytes. + + :returns: bytes object + + .. note:: + + if ``encoding`` is set to ``None``, then unicode strings + will be rejected, and only byte strings will be allowed through. + """ + if isinstance(source, bytes): + if source_encoding and encoding and \ + not is_same_codec(source_encoding, encoding): + return source.decode(source_encoding).encode(encoding) + else: + return source + elif not encoding: + raise TypeError("%s must be bytes, not %s" % (errname, type(source))) + elif isinstance(source, unicode): + return source.encode(encoding) + elif source_encoding: + raise TypeError("%s must be unicode or %s-encoded bytes, not %s" % + (errname, source_encoding, type(source))) + else: + raise TypeError("%s must be unicode or bytes, not %s" % (errname, type(source))) + +def to_unicode(source, source_encoding="utf-8", errname="value"): + """take in unicode or bytes, return unicode + + if bytes provided, decodes using specified encoding. + leaves unicode alone. + + :raises TypeError: if source is not unicode or bytes. + + :arg source: source bytes/unicode to process + :arg source_encoding: encoding to use when decoding bytes instances + :param errname: optional name of variable/noun to reference when raising errors + + :returns: unicode object + """ + if isinstance(source, unicode): + return source + elif not source_encoding: + raise TypeError("%s must be unicode, not %s" % (errname, type(source))) + elif isinstance(source, bytes): + return source.decode(source_encoding) + else: + raise TypeError("%s must be unicode or %s-encoded bytes, not %s" % + (errname, source_encoding, type(source))) + if PY3: - def xor_bytes(left, right): - "perform bitwise-xor of two byte-strings" - return bytes(l ^ r for l, r in zip(left, right)) + def to_native_str(source, encoding="utf-8", errname="value"): + if isinstance(source, bytes): + return source.decode(encoding) + elif isinstance(source, unicode): + return source + else: + raise TypeError("%s must be unicode or bytes, not %s" % + (errname, type(source))) else: - def xor_bytes(left, right): - "perform bitwise-xor of two byte-strings" - return bjoin(chr(ord(l) ^ ord(r)) for l, r in zip(left, right)) + def to_native_str(source, encoding="utf-8", errname="value"): + if isinstance(source, bytes): + return source + elif isinstance(source, unicode): + return source.encode(encoding) + else: + raise TypeError("%s must be unicode or bytes, not %s" % + (errname, type(source))) + +_add_doc(to_native_str, + """take in unicode or bytes, return native string + + python 2: encodes unicode using specified encoding, leaves bytes alone. + python 3: decodes bytes using specified encoding, leaves unicode alone. + + :raises TypeError: if source is not unicode or bytes. + + :arg source: + source unicode or bytes string. + + :arg encoding: + encoding to use when encoding unicode or decoding bytes. + this defaults to ``"utf-8"``. + + :param errname: + optional name of variable/noun to reference when raising errors. + + :returns: :class:`str` instance + """) + +@deprecated_function(deprecated="1.6", removed="1.7") +def to_hash_str(source, encoding="ascii"): + "deprecated, use to_native_str() instead" + return to_native_str(source, encoding, errname="hash") #================================================================================= # base64-variant encoding @@ -1325,6 +1203,70 @@ 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 +except ImportError: #pragma: no cover + safe_os_crypt = os_crypt = None +else: + # NOTE: see docstring below as to why we're wrapping os_crypt() + if PY3: + def safe_os_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. + orig = secret + try: + secret = secret.decode("utf-8") + except UnicodeDecodeError: + return False, 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 + # happen, but being paranoid. + warn("utf-8 password didn't re-encode correctly!") + return False, None + result = os_crypt(secret, hash) + return (result is not None), result + 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. + 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 result is None: + return False, None + else: + return True, result.decode("ascii") + + _add_doc(safe_os_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 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 + :arg hash: hash/salt as unicode + :returns: + ``(False, None)`` if the password can't be hashed (3.x only), + or ``(True, result: unicode)`` otherwise. + """) + # pick best timer function to expose as "tick" - lifted from timeit module. if sys.platform == "win32": # On Windows, the best timer is time.clock() @@ -1481,6 +1423,57 @@ def generate_password(size=10, charset=_52charset): """ return getrandstr(rng, charset, size) +#========================================================== +# object type / interface tests +#========================================================== +_handler_attrs = ( + "name", + "setting_kwds", "context_kwds", + "genconfig", "genhash", + "verify", "encrypt", "identify", + ) + +def is_crypt_handler(obj): + "check if object follows the :ref:`password-hash-api`" + return all(hasattr(obj, name) for name in _handler_attrs) + +_context_attrs = ( + "hash_needs_update", + "genconfig", "genhash", + "verify", "encrypt", "identify", + ) + +def is_crypt_context(obj): + "check if object appears to be a :class:`~passlib.context.CryptContext` instance" + return all(hasattr(obj, name) for name in _context_attrs) + +##def has_many_backends(handler): +## "check if handler provides multiple baceknds" +## #NOTE: should also provide get_backend(), .has_backend(), and .backends attr +## return hasattr(handler, "set_backend") + +def has_rounds_info(handler): + "check if handler provides the optional :ref:`rounds information <optional-rounds-attributes>` attributes" + return ('rounds' in handler.setting_kwds and + getattr(handler, "min_rounds", None) is not None) + +def has_salt_info(handler): + "check if handler provides the optional :ref:`salt information <optional-salt-attributes>` attributes" + return ('salt' in handler.setting_kwds and + getattr(handler, "min_salt_size", None) is not None) + +##def has_raw_salt(handler): +## "check if handler takes in encoded salt as unicode (False), or decoded salt as bytes (True)" +## sc = getattr(handler, "salt_chars", None) +## if sc is None: +## return None +## elif isinstance(sc, unicode): +## return False +## elif isinstance(sc, bytes): +## return True +## else: +## raise TypeError("handler.salt_chars must be None/unicode/bytes") + #================================================================================= # eof #================================================================================= |
