view MoinMoin/support/passlib/utils/__init__.py @ 6133:a6283e189869 tip

fixup: remove nonexisting passlib.utils._blowfish this was removed by the passlib 1.7.1 upgrade.
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Thu, 01 Jun 2017 18:10:19 +0200
parents 7f0616feeae9
children
line wrap: on
line source
"""passlib.utils -- helpers for writing password hashes"""
#=============================================================================
# imports
#=============================================================================
from passlib.utils.compat import JYTHON
# core
from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError
from base64 import b64encode, b64decode
import collections
from codecs import lookup as _lookup_codec
from functools import update_wrapper
import itertools
import inspect
import logging; log = logging.getLogger(__name__)
import math
import os
import sys
import random
import re
if JYTHON: # pragma: no cover -- runtime detection
    # Jython 2.5.2 lacks stringprep module -
    # see http://bugs.jython.org/issue1758320
    try:
        import stringprep
    except ImportError:
        stringprep = None
        _stringprep_missing_reason = "not present under Jython"
else:
    import stringprep
import time
if stringprep:
    import unicodedata
import types
from warnings import warn
# site
# pkg
from passlib.utils.binary import (
    # [remove these aliases in 2.0]
    BASE64_CHARS, AB64_CHARS, HASH64_CHARS, BCRYPT_CHARS,
    Base64Engine, LazyBase64Engine, h64, h64big, bcrypt64,
    ab64_encode, ab64_decode, b64s_encode, b64s_decode
)
from passlib.utils.decor import (
    # [remove these aliases in 2.0]
    deprecated_function,
    deprecated_method,
    memoized_property,
    classproperty,
    hybrid_method,
)
from passlib.exc import ExpectedStringError
from passlib.utils.compat import (add_doc, join_bytes, join_byte_values,
                                  join_byte_elems, irange, imap, PY3, u,
                                  join_unicode, unicode, byte_elem_value, nextgetter,
                                  unicode_or_bytes_types,
                                  get_method_function, suppress_cause)
# local
__all__ = [
    # constants
    'JYTHON',
    'sys_bits',
    'unix_crypt_schemes',
    'rounds_cost_values',

    # unicode helpers
    'consteq',
    'saslprep',

    # bytes helpers
    "xor_bytes",
    "render_bytes",

    # encoding helpers
    'is_same_codec',
    'is_ascii_safe',
    'to_bytes',
    'to_unicode',
    'to_native_str',

    # host OS
    'has_crypt',
    'test_crypt',
    'safe_crypt',
    'tick',

    # randomness
    'rng',
    'getrandbytes',
    'getrandstr',
    'generate_password',

    # object type / interface tests
    'is_crypt_handler',
    'is_crypt_context',
    'has_rounds_info',
    'has_salt_info',
]

#=============================================================================
# constants
#=============================================================================

# 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 algs supported by crypt() on at least one OS.
# XXX: move to .registry for passlib 2.0?
unix_crypt_schemes = [
    "sha512_crypt", "sha256_crypt",
    "sha1_crypt", "bcrypt",
    "md5_crypt",
    # "bsd_nthash",
    "bsdi_crypt", "des_crypt",
    ]

# list of rounds_cost constants
rounds_cost_values = [ "linear", "log2" ]

# legacy import, will be removed in 1.8
from passlib.exc import MissingBackendError

# internal helpers
_BEMPTY = b''
_UEMPTY = u("")
_USPACE = u(" ")

# maximum password size which passlib will allow; see exc.PasswordSizeError
MAX_PASSWORD_SIZE = int(os.environ.get("PASSLIB_MAX_PASSWORD_SIZE") or 4096)

#=============================================================================
# type helpers
#=============================================================================

class SequenceMixin(object):
    """
    helper which lets result object act like a fixed-length sequence.
    subclass just needs to provide :meth:`_as_tuple()`.
    """
    def _as_tuple(self):
        raise NotImplemented("implement in subclass")

    def __repr__(self):
        return repr(self._as_tuple())

    def __getitem__(self, idx):
        return self._as_tuple()[idx]

    def __iter__(self):
        return iter(self._as_tuple())

    def __len__(self):
        return len(self._as_tuple())

    def __eq__(self, other):
        return self._as_tuple() == other

    def __ne__(self, other):
        return not self.__eq__(other)

if PY3:
    # getargspec() is deprecated, use this under py3.
    # even though it's a lot more awkward to get basic info :|

    _VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD
    _VAR_ANY_SET = set([_VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL])

    def accepts_keyword(func, key):
        """test if function accepts specified keyword"""
        params = inspect.signature(get_method_function(func)).parameters
        if not params:
            return False
        arg = params.get(key)
        if arg and arg.kind not in _VAR_ANY_SET:
            return True
        # XXX: annoying what we have to do to determine if VAR_KWDS in use.
        return params[list(params)[-1]].kind == _VAR_KEYWORD

else:

    def accepts_keyword(func, key):
        """test if function accepts specified keyword"""
        spec = inspect.getargspec(get_method_function(func))
        return key in spec.args or spec.keywords is not None

def update_mixin_classes(target, add=None, remove=None, append=False,
                         before=None, after=None, dryrun=False):
    """
    helper to update mixin classes installed in target class.

    :param target:
        target class whose bases will be modified.

    :param add:
        class / classes to install into target's base class list.

    :param remove:
        class / classes to remove from target's base class list.

    :param append:
        by default, prepends mixins to front of list.
        if True, appends to end of list instead.

    :param after:
        optionally make sure all mixins are inserted after
        this class / classes.

    :param before:
        optionally make sure all mixins are inserted before
        this class / classes.

    :param dryrun:
        optionally perform all calculations / raise errors,
        but don't actually modify the class.
    """
    if isinstance(add, type):
        add = [add]

    bases = list(target.__bases__)

    # strip out requested mixins
    if remove:
        if isinstance(remove, type):
            remove = [remove]
        for mixin in remove:
            if add and mixin in add:
                continue
            if mixin in bases:
                bases.remove(mixin)

    # add requested mixins
    if add:
        for mixin in add:
            # if mixin already present (explicitly or not), leave alone
            if any(issubclass(base, mixin) for base in bases):
                continue

            # determine insertion point
            if append:
                for idx, base in enumerate(bases):
                    if issubclass(mixin, base):
                        # don't insert mixin after one of it's own bases
                        break
                    if before and issubclass(base, before):
                        # don't insert mixin after any <before> classes.
                        break
                else:
                    # append to end
                    idx = len(bases)
            elif after:
                for end_idx, base in enumerate(reversed(bases)):
                    if issubclass(base, after):
                        # don't insert mixin before any <after> classes.
                        idx = len(bases) - end_idx
                        assert bases[idx-1] == base
                        break
                else:
                    idx = 0
            else:
                # insert at start
                idx = 0

            # insert mixin
            bases.insert(idx, mixin)

    # modify class
    if not dryrun:
        target.__bases__ = tuple(bases)

#=============================================================================
# collection helpers
#=============================================================================
def batch(source, size):
    """
    split iterable into chunks of <size> elements.
    """
    if size < 1:
        raise ValueError("size must be positive integer")
    if isinstance(source, collections.Sequence):
        end = len(source)
        i = 0
        while i < end:
            n = i + size
            yield source[i:n]
            i = n
    elif isinstance(source, collections.Iterable):
        itr = iter(source)
        while True:
            chunk_itr = itertools.islice(itr, size)
            try:
                first = next(chunk_itr)
            except StopIteration:
                break
            yield itertools.chain((first,), chunk_itr)
    else:
        raise TypeError("source must be iterable")

#=============================================================================
# unicode helpers
#=============================================================================

# XXX: should this be moved to passlib.crypto, or compat backports?

def consteq(left, right):
    """Check two strings/bytes for equality.

    This function uses an approach designed to prevent
    timing analysis, making it appropriate for cryptography.
    a and b must both be of the same type: either str (ASCII only),
    or any type that supports the buffer protocol (e.g. bytes).

    Note: If a and b are of different lengths, or if an error occurs,
    a timing attack could theoretically reveal information about the
    types and lengths of a and b--but not their values.
    """
    # NOTE:
    # resources & discussions considered in the design of this function:
    #   hmac timing attack --
    #       http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/
    #   python developer discussion surrounding similar function --
    #       http://bugs.python.org/issue15061
    #       http://bugs.python.org/issue14955

    # validate types
    if isinstance(left, unicode):
        if not isinstance(right, unicode):
            raise TypeError("inputs must be both unicode or both bytes")
        is_py3_bytes = False
    elif isinstance(left, bytes):
        if not isinstance(right, bytes):
            raise TypeError("inputs must be both unicode or both bytes")
        is_py3_bytes = PY3
    else:
        raise TypeError("inputs must be both unicode or both bytes")

    # do size comparison.
    # NOTE: the double-if construction below is done deliberately, to ensure
    # the same number of operations (including branches) is performed regardless
    # of whether left & right are the same size.
    same_size = (len(left) == len(right))
    if same_size:
        # if sizes are the same, setup loop to perform actual check of contents.
        tmp = left
        result = 0
    if not same_size:
        # if sizes aren't the same, set 'result' so equality will fail regardless
        # of contents. then, to ensure we do exactly 'len(right)' iterations
        # of the loop, just compare 'right' against itself.
        tmp = right
        result = 1

    # run constant-time string comparision
    # TODO: use izip instead (but first verify it's faster than zip for this case)
    if is_py3_bytes:
        for l,r in zip(tmp, right):
            result |= l ^ r
    else:
        for l,r in zip(tmp, right):
            result |= ord(l) ^ ord(r)
    return result == 0

# keep copy of this around since stdlib's version throws error on non-ascii chars in unicode strings.
# our version does, but suffers from some underlying VM issues.  but something is better than
# nothing for plaintext hashes, which need this.  everything else should use consteq(),
# since the stdlib one is going to be as good / better in the general case.
str_consteq = consteq

try:
    # for py3.3 and up, use the stdlib version
    from hmac import compare_digest as consteq
except ImportError:
    pass

    # TODO: could check for cryptography package's version,
    #       but only operates on bytes, so would need a wrapper,
    #       or separate consteq() into a unicode & a bytes variant.
    # from cryptography.hazmat.primitives.constant_time import bytes_eq as consteq

def splitcomma(source, sep=","):
    """split comma-separated string into list of elements,
    stripping whitespace.
    """
    source = source.strip()
    if source.endswith(sep):
        source = source[:-1]
    if not source:
        return []
    return [ elem.strip() for elem in source.split(sep) ]

def saslprep(source, param="value"):
    """Normalizes unicode strings using SASLPrep stringprep profile.

    The SASLPrep profile is defined in :rfc:`4013`.
    It provides a uniform scheme for normalizing unicode usernames
    and passwords before performing byte-value sensitive operations
    such as hashing. Among other things, it normalizes diacritic
    representations, removes non-printing characters, and forbids
    invalid characters such as ``\\n``. Properly internationalized
    applications should run user passwords through this function
    before hashing.

    :arg source:
        unicode string to normalize & validate

    :param param:
        Optional noun identifying source parameter in error messages
        (Defaults to the string ``"value"``). This is mainly useful to make the caller's error
        messages make more sense contextually.

    :raises ValueError:
        if any characters forbidden by the SASLPrep profile are encountered.

    :raises TypeError:
        if input is not :class:`!unicode`

    :returns:
        normalized unicode string

    .. note::

        This function is not available under Jython,
        as the Jython stdlib is missing the :mod:`!stringprep` module
        (`Jython issue 1758320 <http://bugs.jython.org/issue1758320>`_).

    .. versionadded:: 1.6
    """
    # saslprep - http://tools.ietf.org/html/rfc4013
    # stringprep - http://tools.ietf.org/html/rfc3454
    #              http://docs.python.org/library/stringprep.html

    # validate type
    # XXX: support bytes (e.g. run through want_unicode)?
    #      might be easier to just integrate this into cryptcontext.
    if not isinstance(source, unicode):
        raise TypeError("input must be unicode string, not %s" %
                        (type(source),))

    # mapping stage
    #   - map non-ascii spaces to U+0020 (stringprep C.1.2)
    #   - strip 'commonly mapped to nothing' chars (stringprep B.1)
    in_table_c12 = stringprep.in_table_c12
    in_table_b1 = stringprep.in_table_b1
    data = join_unicode(
        _USPACE if in_table_c12(c) else c
        for c in source
        if not in_table_b1(c)
        )

    # normalize to KC form
    data = unicodedata.normalize('NFKC', data)
    if not data:
        return _UEMPTY

    # check for invalid bi-directional strings.
    # stringprep requires the following:
    #   - chars in C.8 must be prohibited.
    #   - if any R/AL chars in string:
    #       - no L chars allowed in string
    #       - first and last must be R/AL chars
    # this checks if start/end are R/AL chars. if so, prohibited loop
    # will forbid all L chars. if not, prohibited loop will forbid all
    # R/AL chars instead. in both cases, prohibited loop takes care of C.8.
    is_ral_char = stringprep.in_table_d1
    if is_ral_char(data[0]):
        if not is_ral_char(data[-1]):
            raise ValueError("malformed bidi sequence in " + param)
        # forbid L chars within R/AL sequence.
        is_forbidden_bidi_char = stringprep.in_table_d2
    else:
        # forbid R/AL chars if start not setup correctly; L chars allowed.
        is_forbidden_bidi_char = is_ral_char

    # check for prohibited output - stringprep tables A.1, B.1, C.1.2, C.2 - C.9
    in_table_a1 = stringprep.in_table_a1
    in_table_c21_c22 = stringprep.in_table_c21_c22
    in_table_c3 = stringprep.in_table_c3
    in_table_c4 = stringprep.in_table_c4
    in_table_c5 = stringprep.in_table_c5
    in_table_c6 = stringprep.in_table_c6
    in_table_c7 = stringprep.in_table_c7
    in_table_c8 = stringprep.in_table_c8
    in_table_c9 = stringprep.in_table_c9
    for c in data:
        # check for chars mapping stage should have removed
        assert not in_table_b1(c), "failed to strip B.1 in mapping stage"
        assert not in_table_c12(c), "failed to replace C.1.2 in mapping stage"

        # check for forbidden chars
        if in_table_a1(c):
            raise ValueError("unassigned code points forbidden in " + param)
        if in_table_c21_c22(c):
            raise ValueError("control characters forbidden in " + param)
        if in_table_c3(c):
            raise ValueError("private use characters forbidden in " + param)
        if in_table_c4(c):
            raise ValueError("non-char code points forbidden in " + param)
        if in_table_c5(c):
            raise ValueError("surrogate codes forbidden in " + param)
        if in_table_c6(c):
            raise ValueError("non-plaintext chars forbidden in " + param)
        if in_table_c7(c):
            # XXX: should these have been caught by normalize?
            # if so, should change this to an assert
            raise ValueError("non-canonical chars forbidden in " + param)
        if in_table_c8(c):
            raise ValueError("display-modifying / deprecated chars "
                             "forbidden in" + param)
        if in_table_c9(c):
            raise ValueError("tagged characters forbidden in " + param)

        # do bidi constraint check chosen by bidi init, above
        if is_forbidden_bidi_char(c):
            raise ValueError("forbidden bidi character in " + param)

    return data

# replace saslprep() with stub when stringprep is missing
if stringprep is None: # pragma: no cover -- runtime detection
    def saslprep(source, param="value"):
        """stub for saslprep()"""
        raise NotImplementedError("saslprep() support requires the 'stringprep' "
                            "module, which is " + _stringprep_missing_reason)

#=============================================================================
# bytes helpers
#=============================================================================
def render_bytes(source, *args):
    """Peform ``%`` formating using bytes in a uniform manner across Python 2/3.

    This function is motivated by the fact that
    :class:`bytes` instances do not support ``%`` or ``{}`` formatting under Python 3.
    This function is an attempt to provide a replacement:
    it converts everything to unicode (decoding bytes instances as ``latin-1``),
    performs the required formatting, then encodes the result to ``latin-1``.

    Calling ``render_bytes(source, *args)`` should function roughly the same as
    ``source % args`` under Python 2.
    """
    if isinstance(source, bytes):
        source = source.decode("latin-1")
    result = source % tuple(arg.decode("latin-1") if isinstance(arg, bytes)
                            else arg for arg in args)
    return result.encode("latin-1")

if PY3:
    # new in py32
    def bytes_to_int(value):
        return int.from_bytes(value, 'big')
    def int_to_bytes(value, count):
        return value.to_bytes(count, 'big')
else:
    # XXX: can any of these be sped up?
    from binascii import hexlify, unhexlify
    def bytes_to_int(value):
        return int(hexlify(value),16)
    def int_to_bytes(value, count):
        return unhexlify(('%%0%dx' % (count<<1)) % value)

add_doc(bytes_to_int, "decode byte string as single big-endian integer")
add_doc(int_to_bytes, "encode integer as single big-endian byte string")

def xor_bytes(left, right):
    """Perform bitwise-xor of two byte strings (must be same size)"""
    return int_to_bytes(bytes_to_int(left) ^ bytes_to_int(right), len(left))

def repeat_string(source, size):
    """repeat or truncate <source> string, so it has length <size>"""
    cur = len(source)
    if size > cur:
        mult = (size+cur-1)//cur
        return (source*mult)[:size]
    else:
        return source[:size]

_BNULL = b"\x00"
_UNULL = u("\x00")

def right_pad_string(source, size, pad=None):
    """right-pad or truncate <source> string, so it has length <size>"""
    cur = len(source)
    if size > cur:
        if pad is None:
            pad = _UNULL if isinstance(source, unicode) else _BNULL
        return source+pad*(size-cur)
    else:
        return source[:size]

#=============================================================================
# encoding helpers
#=============================================================================
_ASCII_TEST_BYTES = b"\x00\n aA:#!\x7f"
_ASCII_TEST_UNICODE = _ASCII_TEST_BYTES.decode("ascii")

def is_ascii_codec(codec):
    """Test if codec is compatible with 7-bit ascii (e.g. latin-1, utf-8; but not utf-16)"""
    return _ASCII_TEST_UNICODE.encode(codec) == _ASCII_TEST_BYTES

def is_same_codec(left, right):
    """Check if two codec 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 = b'\x80'[0]
_U80 = u('\x80')
def is_ascii_safe(source):
    """Check if string (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", param="value", source_encoding=None):
    """Helper to normalize input to bytes.

    :arg source:
        Source bytes/unicode to process.

    :arg encoding:
        Target encoding (defaults to ``"utf-8"``).

    :param param:
        Optional name of variable/noun to reference when raising errors

    :param source_encoding:
        If this is specified, and the source is bytes,
        the source will be transcoded from *source_encoding* to *encoding*
        (via unicode).

    :raises TypeError: if source is not unicode or bytes.

    :returns:
        * unicode strings will be encoded using *encoding*, and returned.
        * if *source_encoding* is not specified, byte strings will be
          returned unchanged.
        * if *source_encoding* is specified, byte strings will be transcoded
          to *encoding*.
    """
    assert encoding
    if isinstance(source, bytes):
        if source_encoding and not is_same_codec(source_encoding, encoding):
            return source.decode(source_encoding).encode(encoding)
        else:
            return source
    elif isinstance(source, unicode):
        return source.encode(encoding)
    else:
        raise ExpectedStringError(source, param)

def to_unicode(source, encoding="utf-8", param="value"):
    """Helper to normalize input to unicode.

    :arg source:
        source bytes/unicode to process.

    :arg encoding:
        encoding to use when decoding bytes instances.

    :param param:
        optional name of variable/noun to reference when raising errors.

    :raises TypeError: if source is not unicode or bytes.

    :returns:
        * returns unicode strings unchanged.
        * returns bytes strings decoded using *encoding*
    """
    assert encoding
    if isinstance(source, unicode):
        return source
    elif isinstance(source, bytes):
        return source.decode(encoding)
    else:
        raise ExpectedStringError(source, param)

if PY3:
    def to_native_str(source, encoding="utf-8", param="value"):
        if isinstance(source, bytes):
            return source.decode(encoding)
        elif isinstance(source, unicode):
            return source
        else:
            raise ExpectedStringError(source, param)
else:
    def to_native_str(source, encoding="utf-8", param="value"):
        if isinstance(source, bytes):
            return source
        elif isinstance(source, unicode):
            return source.encode(encoding)
        else:
            raise ExpectedStringError(source, param)

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: leaves unicode alone, decodes bytes using specified encoding.

    :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 param:
        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"): # pragma: no cover -- deprecated & unused
    """deprecated, use to_native_str() instead"""
    return to_native_str(source, encoding, param="hash")

_true_set = set("true t yes y on 1 enable enabled".split())
_false_set = set("false f no n off 0 disable disabled".split())
_none_set = set(["", "none"])

def as_bool(value, none=None, param="boolean"):
    """
    helper to convert value to boolean.
    recognizes strings such as "true", "false"
    """
    assert none in [True, False, None]
    if isinstance(value, unicode_or_bytes_types):
        clean = value.lower().strip()
        if clean in _true_set:
            return True
        if clean in _false_set:
            return False
        if clean in _none_set:
            return none
        raise ValueError("unrecognized %s value: %r" % (param, value))
    elif isinstance(value, bool):
        return value
    elif value is None:
        return none
    else:
        return bool(value)

#=============================================================================
# host OS helpers
#=============================================================================

try:
    from crypt import crypt as _crypt
except ImportError: # pragma: no cover
    _crypt = None
    has_crypt = False
    def safe_crypt(secret, hash):
        return None
else:
    has_crypt = True
    _NULL = '\x00'

    # some crypt() variants will return various constant strings when
    # an invalid/unrecognized config string is passed in; instead of
    # returning NULL / None. examples include ":", ":0", "*0", etc.
    # safe_crypt() returns None for any string starting with one of the
    # chars in this string...
    _invalid_prefixes = u("*:!")

    if PY3:
        def safe_crypt(secret, hash):
            if isinstance(secret, bytes):
                # 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.
                orig = secret
                try:
                    secret = secret.decode("utf-8")
                except UnicodeDecodeError:
                    return None
                assert secret.encode("utf-8") == orig, \
                            "utf-8 spec says this can't happen!"
            if _NULL in secret:
                raise ValueError("null character in secret")
            if isinstance(hash, bytes):
                hash = hash.decode("ascii")
            result = _crypt(secret, hash)
            if not result or result[0] in _invalid_prefixes:
                return None
            return result
    else:
        def safe_crypt(secret, hash):
            if isinstance(secret, unicode):
                secret = secret.encode("utf-8")
            if _NULL in secret:
                raise ValueError("null character in secret")
            if isinstance(hash, unicode):
                hash = hash.encode("ascii")
            result = _crypt(secret, hash)
            if not result:
                return None
            result = result.decode("ascii")
            if result[0] in _invalid_prefixes:
                return None
            return result

add_doc(safe_crypt, """Wrapper around stdlib's crypt.

    This is a wrapper around stdlib's :func:`!crypt.crypt`, which attempts
    to provide uniform behavior across Python 2 and 3.

    :arg secret:
        password, as bytes or unicode (unicode will be encoded as ``utf-8``).

    :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).

        * Some OSes will return an error string if the input config
          is recognized but malformed; current code converts these to ``None``
          as well.
    """)

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":
    # On Windows, the best timer is time.clock()
    from time import clock as timer
else:
    # On most other platforms the best timer is time.time()
    from time import time as timer

# legacy alias, will be removed in passlib 2.0
tick = timer

def parse_version(source):
    """helper to parse version string"""
    m = re.search(r"(\d+(?:\.\d+)+)", source)
    if m:
        return tuple(int(elem) for elem in m.group(1).split("."))
    return None

#=============================================================================
# randomness
#=============================================================================

#------------------------------------------------------------------------
# setup rng for generating salts
#------------------------------------------------------------------------

# NOTE:
# generating salts (e.g. h64_gensalt, below) doesn't require cryptographically
# strong randomness. it just requires enough range of possible outputs
# that making a rainbow table is too costly. so it should be ok to
# fall back on python's builtin mersenne twister prng, as long as it's seeded each time
# this module is imported, using a couple of minor entropy sources.

try:
    os.urandom(1)
    has_urandom = True
except NotImplementedError: # pragma: no cover
    has_urandom = False

def genseed(value=None):
    """generate prng seed value from system resources"""
    from hashlib import sha512
    if hasattr(value, "getstate") and hasattr(value, "getrandbits"):
        # caller passed in RNG as seed value
        try:
            value = value.getstate()
        except NotImplementedError:
            # this method throws error for e.g. SystemRandom instances,
            # so fall back to extracting 4k of state
            value = value.getrandbits(1 << 15)
    text = u("%s %s %s %.15f %.15f %s") % (
        # if caller specified a seed value, mix it in
        value,

        # add current process id
        # NOTE: not available in some environments, e.g. GAE
        os.getpid() if hasattr(os, "getpid") else None,

        # id of a freshly created object.
        # (at least 1 byte of which should be hard to predict)
        id(object()),

        # the current time, to whatever precision os uses
        time.time(),
        time.clock(),

        # if urandom available, might as well mix some bytes in.
        os.urandom(32).decode("latin-1") if has_urandom else 0,
        )
    # hash it all up and return it as int/long
    return int(sha512(text.encode("utf-8")).hexdigest(), 16)

if has_urandom:
    rng = random.SystemRandom()
else: # pragma: no cover -- runtime detection
    # NOTE: to reseed use ``rng.seed(genseed(rng))``
    # XXX: could reseed on every call
    rng = random.Random(genseed())

#------------------------------------------------------------------------
# some rng helpers
#------------------------------------------------------------------------
def getrandbytes(rng, count):
    """return byte-string containing *count* number of randomly generated bytes, using specified rng"""
    # NOTE: would be nice if this was present in stdlib Random class

    ###just in case rng provides this...
    ##meth = getattr(rng, "getrandbytes", None)
    ##if meth:
    ##    return meth(count)

    if not count:
        return _BEMPTY
    def helper():
        # XXX: break into chunks for large number of bits?
        value = rng.getrandbits(count<<3)
        i = 0
        while i < count:
            yield value & 0xff
            value >>= 3
            i += 1
    return join_byte_values(helper())

def getrandstr(rng, charset, count):
    """return string containing *count* number of chars/bytes, whose elements are drawn from specified charset, using specified rng"""
    # NOTE: tests determined this is 4x faster than rng.sample(),
    # which is why that's not being used here.

    # check alphabet & count
    if count < 0:
        raise ValueError("count must be >= 0")
    letters = len(charset)
    if letters == 0:
        raise ValueError("alphabet must not be empty")
    if letters == 1:
        return charset * count

    # get random value, and write out to buffer
    def helper():
        # XXX: break into chunks for large number of letters?
        value = rng.randrange(0, letters**count)
        i = 0
        while i < count:
            yield charset[value % letters]
            value //= letters
            i += 1

    if isinstance(charset, unicode):
        return join_unicode(helper())
    else:
        return join_byte_elems(helper())

_52charset = '2346789ABCDEFGHJKMNPQRTUVWXYZabcdefghjkmnpqrstuvwxyz'

@deprecated_function(deprecated="1.7", removed="2.0",
                     replacement="passlib.pwd.genword() / passlib.pwd.genphrase()")
def generate_password(size=10, charset=_52charset):
    """generate random password using given length & charset

    :param size:
        size of password.

    :param charset:
        optional string specified set of characters to draw from.

        the default charset contains all normal alphanumeric characters,
        except for the characters ``1IiLl0OoS5``, which were omitted
        due to their visual similarity.

    :returns: :class:`!str` containing randomly generated password.

    .. note::

        Using the default character set, on a OS with :class:`!SystemRandom` support,
        this function should generate passwords with 5.7 bits of entropy per character.
    """
    return getrandstr(rng, charset, size)

#=============================================================================
# object type / interface tests
#=============================================================================
_handler_attrs = (
        "name",
        "setting_kwds", "context_kwds",
        "verify", "hash", "identify",
        )

def is_crypt_handler(obj):
    """check if object follows the :ref:`password-hash-api`"""
    # XXX: change to use isinstance(obj, PasswordHash) under py26+?
    return all(hasattr(obj, name) for name in _handler_attrs)

_context_attrs = (
        "needs_update",
        "genconfig", "genhash",
        "verify", "encrypt", "identify",
        )

def is_crypt_context(obj):
    """check if object appears to be a :class:`~passlib.context.CryptContext` instance"""
    # XXX: change to use isinstance(obj, CryptContext)?
    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 <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 <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
#=============================================================================