From d8f704a41915e31934aa73b606a775cdcff5a95a Mon Sep 17 00:00:00 2001 From: Polina Date: Thu, 2 Jul 2026 18:53:27 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?venv/Lib/site-packages/passlib/utils=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../site-packages/passlib/utils/__init__.py | 1220 ++++++++ .../Lib/site-packages/passlib/utils/binary.py | 884 ++++++ venv/Lib/site-packages/passlib/utils/decor.py | 233 ++ venv/Lib/site-packages/passlib/utils/des.py | 46 + .../site-packages/passlib/utils/handlers.py | 2711 +++++++++++++++++ 5 files changed, 5094 insertions(+) create mode 100644 venv/Lib/site-packages/passlib/utils/__init__.py create mode 100644 venv/Lib/site-packages/passlib/utils/binary.py create mode 100644 venv/Lib/site-packages/passlib/utils/decor.py create mode 100644 venv/Lib/site-packages/passlib/utils/des.py create mode 100644 venv/Lib/site-packages/passlib/utils/handlers.py diff --git a/venv/Lib/site-packages/passlib/utils/__init__.py b/venv/Lib/site-packages/passlib/utils/__init__.py new file mode 100644 index 0000000..6147886 --- /dev/null +++ b/venv/Lib/site-packages/passlib/utils/__init__.py @@ -0,0 +1,1220 @@ +"""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 +try: + from collections.abc import Sequence + from collections.abc import Iterable +except ImportError: + # py2 compat + from collections import Sequence + from collections import Iterable +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 +try: + import threading +except ImportError: + # module optional before py37 + threading = None +import timeit +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, ExpectedTypeError +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_str, unicode_or_bytes_types, + get_method_function, suppress_cause, PYPY) +# 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 NotImplementedError("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 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 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 elements. + """ + if size < 1: + raise ValueError("size must be positive integer") + if isinstance(source, Sequence): + end = len(source) + i = 0 + while i < end: + n = i + size + yield source[i:n] + i = n + elif isinstance(source, 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 `_). + + .. 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. + + .. todo:: + python >= 3.5 added back limited support for bytes %, + can revisit when 3.3/3.4 is dropped. + """ + 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 string, so it has length + """ + mult = 1 + (size - 1) // len(source) + return (source * mult)[:size] + + +def utf8_repeat_string(source, size): + """ + variant of repeat_string() which truncates to nearest UTF8 boundary. + """ + mult = 1 + (size - 1) // len(source) + return utf8_truncate(source * mult, size) + + +_BNULL = b"\x00" +_UNULL = u("\x00") + +def right_pad_string(source, size, pad=None): + """right-pad or truncate string, so it has length """ + 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] + + +def utf8_truncate(source, index): + """ + helper to truncate UTF8 byte string to nearest character boundary ON OR AFTER . + returned prefix will always have length of at least , and will stop on the + first byte that's not a UTF8 continuation byte (128 - 191 inclusive). + since utf8 should never take more than 4 bytes to encode known unicode values, + we can stop after ``index+3`` is reached. + + :param bytes source: + :param int index: + :rtype: bytes + """ + # general approach: + # + # * UTF8 bytes will have high two bits (0xC0) as one of: + # 00 -- ascii char + # 01 -- ascii char + # 10 -- continuation of multibyte char + # 11 -- start of multibyte char. + # thus we can cut on anything where high bits aren't "10" (0x80; continuation byte) + # + # * UTF8 characters SHOULD always be 1 to 4 bytes, though they may be unbounded. + # so we just keep going until first non-continuation byte is encountered, or end of str. + # this should work predictably even for malformed/non UTF8 inputs. + + if not isinstance(source, bytes): + raise ExpectedTypeError(source, bytes, "source") + + # validate index + end = len(source) + if index < 0: + index = max(0, index + end) + if index >= end: + return source + + # can stop search after 4 bytes, won't ever have longer utf8 sequence. + end = min(index + 3, end) + + # loop until we find non-continuation byte + while index < end: + if byte_elem_value(source[index]) & 0xC0 != 0x80: + # found single-char byte, or start-char byte. + break + # else: found continuation byte. + index += 1 + else: + assert index == end + + # truncate at final index + result = source[:index] + + def sanity_check(): + # try to decode source + try: + text = source.decode("utf-8") + except UnicodeDecodeError: + # if source isn't valid utf8, byte level match is enough + return True + + # validate that result was cut on character boundary + assert text.startswith(result.decode("utf-8")) + return True + + assert sanity_check() + + return result + +#============================================================================= +# 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 +#============================================================================= + +def is_safe_crypt_input(value): + """ + UT helper -- + test if value is safe to pass to crypt.crypt(); + under PY3, can't pass non-UTF8 bytes to crypt.crypt. + """ + if crypt_accepts_bytes or not isinstance(value, bytes): + return True + try: + value.decode("utf-8") + return True + except UnicodeDecodeError: + return False + +try: + from crypt import crypt as _crypt +except ImportError: # pragma: no cover + _crypt = None + has_crypt = False + crypt_accepts_bytes = False + crypt_needs_lock = False + _safe_crypt_lock = None + def safe_crypt(secret, hash): + return None +else: + has_crypt = True + _NULL = '\x00' + + # XXX: replace this with lazy-evaluated bug detection? + if threading and PYPY and (7, 2, 0) <= sys.pypy_version_info <= (7, 3, 3): + #: internal lock used to wrap crypt() calls. + #: WARNING: if non-passlib code invokes crypt(), this lock won't be enough! + _safe_crypt_lock = threading.Lock() + + #: detect if crypt.crypt() needs a thread lock around calls. + crypt_needs_lock = True + + else: + from passlib.utils.compat import nullcontext + _safe_crypt_lock = nullcontext() + crypt_needs_lock = False + + # 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: + + # * pypy3 (as of v7.3.1) has a crypt which accepts bytes, or ASCII-only unicode. + # * whereas CPython3 (as of v3.9) has a crypt which doesn't take bytes, + # but accepts ANY unicode (which it always encodes to UTF8). + crypt_accepts_bytes = True + try: + _crypt(b"\xEE", "xx") + except TypeError: + # CPython will throw TypeError + crypt_accepts_bytes = False + except: # no pragma + # don't care about other errors this might throw, + # just want to see if we get past initial type-coercion step. + pass + + def safe_crypt(secret, hash): + if crypt_accepts_bytes: + # PyPy3 -- all bytes accepted, but unicode encoded to ASCII, + # so handling that ourselves. + if isinstance(secret, unicode): + secret = secret.encode("utf-8") + if _BNULL in secret: + raise ValueError("null character in secret") + if isinstance(hash, unicode): + hash = hash.encode("ascii") + else: + # CPython3's crypt() doesn't take bytes, only unicode; unicode which is then + # encoding using utf-8 before passing to the C-level crypt(). + # so we have to decode the secret. + if isinstance(secret, bytes): + orig = secret + try: + secret = secret.decode("utf-8") + except UnicodeDecodeError: + return None + # sanity check it encodes back to original byte string, + # otherwise when crypt() does it's encoding, it'll hash the wrong bytes! + 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") + try: + with _safe_crypt_lock: + result = _crypt(secret, hash) + except OSError: + # new in py39 -- per https://bugs.python.org/issue39289, + # crypt() now throws OSError for various things, mainly unknown hash formats + # translating that to None for now (may revise safe_crypt behavior in future) + return None + # NOTE: per issue 113, crypt() may return bytes in some odd cases. + # assuming it should still return an ASCII hash though, + # or there's a bigger issue at hand. + if isinstance(result, bytes): + result = result.decode("ascii") + if not result or result[0] in _invalid_prefixes: + return None + return result + else: + + #: see feature-detection in PY3 fork above + crypt_accepts_bytes = True + + # Python 2 crypt handler + 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") + with _safe_crypt_lock: + 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 + """ + # safe_crypt() always returns unicode, which means that for py3, + # 'hash' can't be bytes, or "== hash" will never be True. + # under py2 unicode & str(bytes) will compare fine; + # so just enforcing "unicode_or_str" limitation + assert isinstance(hash, unicode_or_str), \ + "hash must be unicode_or_str, got %s" % type(hash) + assert hash, "hash must be non-empty" + return safe_crypt(secret, hash) == hash + +timer = timeit.default_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(), + tick(), + + # 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 ` 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 ` 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 +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/utils/binary.py b/venv/Lib/site-packages/passlib/utils/binary.py new file mode 100644 index 0000000..521b64a --- /dev/null +++ b/venv/Lib/site-packages/passlib/utils/binary.py @@ -0,0 +1,884 @@ +""" +passlib.utils.binary - binary data encoding/decoding/manipulation +""" +#============================================================================= +# imports +#============================================================================= +# core +from __future__ import absolute_import, division, print_function +from base64 import ( + b64encode, + b64decode, + b32decode as _b32decode, + b32encode as _b32encode, +) +from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError +import logging +log = logging.getLogger(__name__) +# site +# pkg +from passlib import exc +from passlib.utils.compat import ( + PY3, bascii_to_str, + irange, imap, iter_byte_chars, join_byte_values, join_byte_elems, + nextgetter, suppress_cause, + u, unicode, unicode_or_bytes_types, +) +from passlib.utils.decor import memoized_property +# from passlib.utils import BASE64_CHARS, HASH64_CHARS +# local +__all__ = [ + # constants + "BASE64_CHARS", "PADDED_BASE64_CHARS", + "AB64_CHARS", + "HASH64_CHARS", + "BCRYPT_CHARS", + "HEX_CHARS", "LOWER_HEX_CHARS", "UPPER_HEX_CHARS", + + "ALL_BYTE_VALUES", + + # misc + "compile_byte_translation", + + # base64 + 'ab64_encode', 'ab64_decode', + 'b64s_encode', 'b64s_decode', + + # base32 + "b32encode", "b32decode", + + # custom encodings + 'Base64Engine', + 'LazyBase64Engine', + 'h64', + 'h64big', + 'bcrypt64', +] + +#============================================================================= +# constant strings +#============================================================================= + +#------------------------------------------------------------- +# common salt_chars & checksum_chars values +#------------------------------------------------------------- + +#: standard base64 charmap +BASE64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + +#: alt base64 charmap -- "." instead of "+" +AB64_CHARS = u("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./") + +#: charmap used by HASH64 encoding. +HASH64_CHARS = u("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") + +#: charmap used by BCrypt +BCRYPT_CHARS = u("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + +#: std base64 chars + padding char +PADDED_BASE64_CHARS = BASE64_CHARS + u("=") + +#: all hex chars +HEX_CHARS = u("0123456789abcdefABCDEF") + +#: upper case hex chars +UPPER_HEX_CHARS = u("0123456789ABCDEF") + +#: lower case hex chars +LOWER_HEX_CHARS = u("0123456789abcdef") + +#------------------------------------------------------------- +# byte strings +#------------------------------------------------------------- + +#: special byte string containing all possible byte values +#: NOTE: for efficiency, this is treated as singleton by some of the code +ALL_BYTE_VALUES = join_byte_values(irange(256)) + +#: some string constants we reuse +B_EMPTY = b'' +B_NULL = b'\x00' +B_EQUAL = b'=' + +#============================================================================= +# byte translation +#============================================================================= + +#: base list used to compile byte translations +_TRANSLATE_SOURCE = list(iter_byte_chars(ALL_BYTE_VALUES)) + +def compile_byte_translation(mapping, source=None): + """ + return a 256-byte string for translating bytes using specified mapping. + bytes not specified by mapping will be left alone. + + :param mapping: + dict mapping input byte (str or int) -> output byte (str or int). + + :param source: + optional existing byte translation string to use as base. + (must be 255-length byte string). defaults to identity mapping. + + :returns: + 255-length byte string for passing to bytes().translate. + """ + if source is None: + target = _TRANSLATE_SOURCE[:] + else: + assert isinstance(source, bytes) and len(source) == 255 + target = list(iter_byte_chars(source)) + for k, v in mapping.items(): + if isinstance(k, unicode_or_bytes_types): + k = ord(k) + assert isinstance(k, int) and 0 <= k < 256 + if isinstance(v, unicode): + v = v.encode("ascii") + assert isinstance(v, bytes) and len(v) == 1 + target[k] = v + return B_EMPTY.join(target) + +#============================================================================= +# unpadding / stripped base64 encoding +#============================================================================= +def b64s_encode(data): + """ + encode using shortened base64 format which omits padding & whitespace. + uses default ``+/`` altchars. + """ + return b2a_base64(data).rstrip(_BASE64_STRIP) + +def b64s_decode(data): + """ + decode from shortened base64 format which omits padding & whitespace. + uses default ``+/`` altchars. + """ + if isinstance(data, unicode): + # needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64() + try: + data = data.encode("ascii") + except UnicodeEncodeError: + raise suppress_cause(ValueError("string argument should contain only ASCII characters")) + off = len(data) & 3 + if off == 0: + pass + elif off == 2: + data += _BASE64_PAD2 + elif off == 3: + data += _BASE64_PAD1 + else: # off == 1 + raise ValueError("invalid base64 input") + try: + return a2b_base64(data) + except _BinAsciiError as err: + raise suppress_cause(TypeError(err)) + +#============================================================================= +# adapted-base64 encoding +#============================================================================= +_BASE64_STRIP = b"=\n" +_BASE64_PAD1 = b"=" +_BASE64_PAD2 = b"==" + +# XXX: Passlib 1.8/1.9 -- deprecate everything that's using ab64_encode(), +# have it start outputing b64s_encode() instead? can use a64_decode() to retain backwards compat. + +def ab64_encode(data): + """ + encode using shortened base64 format which omits padding & whitespace. + uses custom ``./`` altchars. + + it is primarily used by Passlib's custom pbkdf2 hashes. + """ + return b64s_encode(data).replace(b"+", b".") + +def ab64_decode(data): + """ + decode from shortened base64 format which omits padding & whitespace. + uses custom ``./`` altchars, but supports decoding normal ``+/`` altchars as well. + + it is primarily used by Passlib's custom pbkdf2 hashes. + """ + if isinstance(data, unicode): + # needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64() + try: + data = data.encode("ascii") + except UnicodeEncodeError: + raise suppress_cause(ValueError("string argument should contain only ASCII characters")) + return b64s_decode(data.replace(b".", b"+")) + +#============================================================================= +# base32 codec +#============================================================================= + +def b32encode(source): + """ + wrapper around :func:`base64.b32encode` which strips padding, + and returns a native string. + """ + # NOTE: using upper case by default here, since 'I & L' are less + # visually ambiguous than 'i & l' + return bascii_to_str(_b32encode(source).rstrip(B_EQUAL)) + +#: byte translation map to replace common mistyped base32 chars. +#: XXX: could correct '1' -> 'I', but could be a mistyped lower-case 'l', so leaving it alone. +_b32_translate = compile_byte_translation({"8": "B", "0": "O"}) + +#: helper to add padding +_b32_decode_pad = B_EQUAL * 8 + +def b32decode(source): + """ + wrapper around :func:`base64.b32decode` + which handles common mistyped chars. + padding optional, ignored if present. + """ + # encode & correct for typos + if isinstance(source, unicode): + source = source.encode("ascii") + source = source.translate(_b32_translate) + + # pad things so final string is multiple of 8 + remainder = len(source) & 0x7 + if remainder: + source += _b32_decode_pad[:-remainder] + + # XXX: py27 stdlib's version of this has some inefficiencies, + # could look into using optimized version. + return _b32decode(source, True) + +#============================================================================= +# base64-variant encoding +#============================================================================= + +class Base64Engine(object): + """Provides routines for encoding/decoding base64 data using + arbitrary character mappings, selectable endianness, etc. + + :arg charmap: + A string of 64 unique characters, + which will be used to encode successive 6-bit chunks of data. + A character's position within the string should correspond + to its 6-bit value. + + :param big: + Whether the encoding should be big-endian (default False). + + .. note:: + This class does not currently handle base64's padding characters + in any way what so ever. + + Raw Bytes <-> Encoded Bytes + =========================== + The following methods convert between raw bytes, + and strings encoded using the engine's specific base64 variant: + + .. automethod:: encode_bytes + .. automethod:: decode_bytes + .. automethod:: encode_transposed_bytes + .. automethod:: decode_transposed_bytes + + .. + .. automethod:: check_repair_unused + .. automethod:: repair_unused + + Integers <-> Encoded Bytes + ========================== + The following methods allow encoding and decoding + unsigned integers to and from the engine's specific base64 variant. + Endianess is determined by the engine's ``big`` constructor keyword. + + .. automethod:: encode_int6 + .. automethod:: decode_int6 + + .. automethod:: encode_int12 + .. automethod:: decode_int12 + + .. automethod:: encode_int24 + .. automethod:: decode_int24 + + .. automethod:: encode_int64 + .. automethod:: decode_int64 + + Informational Attributes + ======================== + .. attribute:: charmap + + unicode string containing list of characters used in encoding; + position in string matches 6bit value of character. + + .. attribute:: bytemap + + bytes version of :attr:`charmap` + + .. attribute:: big + + boolean flag indicating this using big-endian encoding. + """ + + #=================================================================== + # instance attrs + #=================================================================== + # public config + bytemap = None # charmap as bytes + big = None # little or big endian + + # filled in by init based on charmap. + # (byte elem: single byte under py2, 8bit int under py3) + _encode64 = None # maps 6bit value -> byte elem + _decode64 = None # maps byte elem -> 6bit value + + # helpers filled in by init based on endianness + _encode_bytes = None # throws IndexError if bad value (shouldn't happen) + _decode_bytes = None # throws KeyError if bad char. + + #=================================================================== + # init + #=================================================================== + def __init__(self, charmap, big=False): + # validate charmap, generate encode64/decode64 helper functions. + if isinstance(charmap, unicode): + charmap = charmap.encode("latin-1") + elif not isinstance(charmap, bytes): + raise exc.ExpectedStringError(charmap, "charmap") + if len(charmap) != 64: + raise ValueError("charmap must be 64 characters in length") + if len(set(charmap)) != 64: + raise ValueError("charmap must not contain duplicate characters") + self.bytemap = charmap + self._encode64 = charmap.__getitem__ + lookup = dict((value, idx) for idx, value in enumerate(charmap)) + self._decode64 = lookup.__getitem__ + + # validate big, set appropriate helper functions. + self.big = big + if big: + self._encode_bytes = self._encode_bytes_big + self._decode_bytes = self._decode_bytes_big + else: + self._encode_bytes = self._encode_bytes_little + self._decode_bytes = self._decode_bytes_little + + # TODO: support padding character + ##if padding is not None: + ## if isinstance(padding, unicode): + ## padding = padding.encode("latin-1") + ## elif not isinstance(padding, bytes): + ## raise TypeError("padding char must be unicode or bytes") + ## if len(padding) != 1: + ## raise ValueError("padding must be single character") + ##self.padding = padding + + @property + def charmap(self): + """charmap as unicode""" + return self.bytemap.decode("latin-1") + + #=================================================================== + # encoding byte strings + #=================================================================== + def encode_bytes(self, source): + """encode bytes to base64 string. + + :arg source: byte string to encode. + :returns: byte string containing encoded data. + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + chunks, tail = divmod(len(source), 3) + if PY3: + next_value = nextgetter(iter(source)) + else: + next_value = nextgetter(ord(elem) for elem in source) + gen = self._encode_bytes(next_value, chunks, tail) + out = join_byte_elems(imap(self._encode64, gen)) + ##if tail: + ## padding = self.padding + ## if padding: + ## out += padding * (3-tail) + return out + + def _encode_bytes_little(self, next_value, chunks, tail): + """helper used by encode_bytes() to handle little-endian encoding""" + # + # output bit layout: + # + # first byte: v1 543210 + # + # second byte: v1 ....76 + # +v2 3210.. + # + # third byte: v2 ..7654 + # +v3 10.... + # + # fourth byte: v3 765432 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + yield v1 & 0x3f + yield ((v2 & 0x0f)<<2)|(v1>>6) + yield ((v3 & 0x03)<<4)|(v2>>4) + yield v3>>2 + idx += 1 + if tail: + v1 = next_value() + if tail == 1: + # note: 4 msb of last byte are padding + yield v1 & 0x3f + yield v1>>6 + else: + assert tail == 2 + # note: 2 msb of last byte are padding + v2 = next_value() + yield v1 & 0x3f + yield ((v2 & 0x0f)<<2)|(v1>>6) + yield v2>>4 + + def _encode_bytes_big(self, next_value, chunks, tail): + """helper used by encode_bytes() to handle big-endian encoding""" + # + # output bit layout: + # + # first byte: v1 765432 + # + # second byte: v1 10.... + # +v2 ..7654 + # + # third byte: v2 3210.. + # +v3 ....76 + # + # fourth byte: v3 543210 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + yield v1>>2 + yield ((v1&0x03)<<4)|(v2>>4) + yield ((v2&0x0f)<<2)|(v3>>6) + yield v3 & 0x3f + idx += 1 + if tail: + v1 = next_value() + if tail == 1: + # note: 4 lsb of last byte are padding + yield v1>>2 + yield (v1&0x03)<<4 + else: + assert tail == 2 + # note: 2 lsb of last byte are padding + v2 = next_value() + yield v1>>2 + yield ((v1&0x03)<<4)|(v2>>4) + yield ((v2&0x0f)<<2) + + #=================================================================== + # decoding byte strings + #=================================================================== + + def decode_bytes(self, source): + """decode bytes from base64 string. + + :arg source: byte string to decode. + :returns: byte string containing decoded data. + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + ##padding = self.padding + ##if padding: + ## # TODO: add padding size check? + ## source = source.rstrip(padding) + chunks, tail = divmod(len(source), 4) + if tail == 1: + # only 6 bits left, can't encode a whole byte! + raise ValueError("input string length cannot be == 1 mod 4") + next_value = nextgetter(imap(self._decode64, source)) + try: + return join_byte_values(self._decode_bytes(next_value, chunks, tail)) + except KeyError as err: + raise ValueError("invalid character: %r" % (err.args[0],)) + + def _decode_bytes_little(self, next_value, chunks, tail): + """helper used by decode_bytes() to handle little-endian encoding""" + # + # input bit layout: + # + # first byte: v1 ..543210 + # +v2 10...... + # + # second byte: v2 ....5432 + # +v3 3210.... + # + # third byte: v3 ......54 + # +v4 543210.. + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + v4 = next_value() + yield v1 | ((v2 & 0x3) << 6) + yield (v2>>2) | ((v3 & 0xF) << 4) + yield (v3>>4) | (v4<<2) + idx += 1 + if tail: + # tail is 2 or 3 + v1 = next_value() + v2 = next_value() + yield v1 | ((v2 & 0x3) << 6) + # NOTE: if tail == 2, 4 msb of v2 are ignored (should be 0) + if tail == 3: + # NOTE: 2 msb of v3 are ignored (should be 0) + v3 = next_value() + yield (v2>>2) | ((v3 & 0xF) << 4) + + def _decode_bytes_big(self, next_value, chunks, tail): + """helper used by decode_bytes() to handle big-endian encoding""" + # + # input bit layout: + # + # first byte: v1 543210.. + # +v2 ......54 + # + # second byte: v2 3210.... + # +v3 ....5432 + # + # third byte: v3 10...... + # +v4 ..543210 + # + idx = 0 + while idx < chunks: + v1 = next_value() + v2 = next_value() + v3 = next_value() + v4 = next_value() + yield (v1<<2) | (v2>>4) + yield ((v2&0xF)<<4) | (v3>>2) + yield ((v3&0x3)<<6) | v4 + idx += 1 + if tail: + # tail is 2 or 3 + v1 = next_value() + v2 = next_value() + yield (v1<<2) | (v2>>4) + # NOTE: if tail == 2, 4 lsb of v2 are ignored (should be 0) + if tail == 3: + # NOTE: 2 lsb of v3 are ignored (should be 0) + v3 = next_value() + yield ((v2&0xF)<<4) | (v3>>2) + + #=================================================================== + # encode/decode helpers + #=================================================================== + + # padmap2/3 - dict mapping last char of string -> + # equivalent char with no padding bits set. + + def __make_padset(self, bits): + """helper to generate set of valid last chars & bytes""" + pset = set(c for i,c in enumerate(self.bytemap) if not i & bits) + pset.update(c for i,c in enumerate(self.charmap) if not i & bits) + return frozenset(pset) + + @memoized_property + def _padinfo2(self): + """mask to clear padding bits, and valid last bytes (for strings 2 % 4)""" + # 4 bits of last char unused (lsb for big, msb for little) + bits = 15 if self.big else (15<<2) + return ~bits, self.__make_padset(bits) + + @memoized_property + def _padinfo3(self): + """mask to clear padding bits, and valid last bytes (for strings 3 % 4)""" + # 2 bits of last char unused (lsb for big, msb for little) + bits = 3 if self.big else (3<<4) + return ~bits, self.__make_padset(bits) + + def check_repair_unused(self, source): + """helper to detect & clear invalid unused bits in last character. + + :arg source: + encoded data (as ascii bytes or unicode). + + :returns: + `(True, result)` if the string was repaired, + `(False, source)` if the string was ok as-is. + """ + # figure out how many padding bits there are in last char. + tail = len(source) & 3 + if tail == 2: + mask, padset = self._padinfo2 + elif tail == 3: + mask, padset = self._padinfo3 + elif not tail: + return False, source + else: + raise ValueError("source length must != 1 mod 4") + + # check if last char is ok (padset contains bytes & unicode versions) + last = source[-1] + if last in padset: + return False, source + + # we have dirty bits - repair the string by decoding last char, + # clearing the padding bits via , and encoding new char. + if isinstance(source, unicode): + cm = self.charmap + last = cm[cm.index(last) & mask] + assert last in padset, "failed to generate valid padding char" + else: + # NOTE: this assumes ascii-compat encoding, and that + # all chars used by encoding are 7-bit ascii. + last = self._encode64(self._decode64(last) & mask) + assert last in padset, "failed to generate valid padding char" + if PY3: + last = bytes([last]) + return True, source[:-1] + last + + def repair_unused(self, source): + return self.check_repair_unused(source)[1] + + ##def transcode(self, source, other): + ## return ''.join( + ## other.charmap[self.charmap.index(char)] + ## for char in source + ## ) + + ##def random_encoded_bytes(self, size, random=None, unicode=False): + ## "return random encoded string of given size" + ## data = getrandstr(random or rng, + ## self.charmap if unicode else self.bytemap, size) + ## return self.repair_unused(data) + + #=================================================================== + # transposed encoding/decoding + #=================================================================== + def encode_transposed_bytes(self, source, offsets): + """encode byte string, first transposing source using offset list""" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + tmp = join_byte_elems(source[off] for off in offsets) + return self.encode_bytes(tmp) + + def decode_transposed_bytes(self, source, offsets): + """decode byte string, then reverse transposition described by offset list""" + # NOTE: if transposition does not use all bytes of source, + # the original can't be recovered... and join_byte_elems() will throw + # an error because 1+ values in will be None. + tmp = self.decode_bytes(source) + buf = [None] * len(offsets) + for off, char in zip(offsets, tmp): + buf[off] = char + return join_byte_elems(buf) + + #=================================================================== + # integer decoding helpers - mainly used by des_crypt family + #=================================================================== + def _decode_int(self, source, bits): + """decode base64 string -> integer + + :arg source: base64 string to decode. + :arg bits: number of bits in resulting integer. + + :raises ValueError: + * if the string contains invalid base64 characters. + * if the string is not long enough - it must be at least + ``int(ceil(bits/6))`` in length. + + :returns: + a integer in the range ``0 <= n < 2**bits`` + """ + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + big = self.big + pad = -bits % 6 + chars = (bits+pad)/6 + if len(source) != chars: + raise ValueError("source must be %d chars" % (chars,)) + decode = self._decode64 + out = 0 + try: + for c in source if big else reversed(source): + out = (out<<6) + decode(c) + except KeyError: + raise ValueError("invalid character in string: %r" % (c,)) + if pad: + # strip padding bits + if big: + out >>= pad + else: + out &= (1< 6 bit integer""" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 1: + raise ValueError("source must be exactly 1 byte") + if PY3: + # convert to 8bit int before doing lookup + source = source[0] + try: + return self._decode64(source) + except KeyError: + raise ValueError("invalid character") + + def decode_int12(self, source): + """decodes 2 char string -> 12-bit integer""" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 2: + raise ValueError("source must be exactly 2 bytes") + decode = self._decode64 + try: + if self.big: + return decode(source[1]) + (decode(source[0])<<6) + else: + return decode(source[0]) + (decode(source[1])<<6) + except KeyError: + raise ValueError("invalid character") + + def decode_int24(self, source): + """decodes 4 char string -> 24-bit integer""" + if not isinstance(source, bytes): + raise TypeError("source must be bytes, not %s" % (type(source),)) + if len(source) != 4: + raise ValueError("source must be exactly 4 bytes") + decode = self._decode64 + try: + if self.big: + return decode(source[3]) + (decode(source[2])<<6)+ \ + (decode(source[1])<<12) + (decode(source[0])<<18) + else: + return decode(source[0]) + (decode(source[1])<<6)+ \ + (decode(source[2])<<12) + (decode(source[3])<<18) + except KeyError: + raise ValueError("invalid character") + + def decode_int30(self, source): + """decode 5 char string -> 30 bit integer""" + return self._decode_int(source, 30) + + def decode_int64(self, source): + """decode 11 char base64 string -> 64-bit integer + + this format is used primarily by des-crypt & variants to encode + the DES output value used as a checksum. + """ + return self._decode_int(source, 64) + + #=================================================================== + # integer encoding helpers - mainly used by des_crypt family + #=================================================================== + def _encode_int(self, value, bits): + """encode integer into base64 format + + :arg value: non-negative integer to encode + :arg bits: number of bits to encode + + :returns: + a string of length ``int(ceil(bits/6.0))``. + """ + assert value >= 0, "caller did not sanitize input" + pad = -bits % 6 + bits += pad + if self.big: + itr = irange(bits-6, -6, -6) + # shift to add lsb padding. + value <<= pad + else: + itr = irange(0, bits, 6) + # padding is msb, so no change needed. + return join_byte_elems(imap(self._encode64, + ((value>>off) & 0x3f for off in itr))) + + #--------------------------------------------------------------- + # optimized versions for common integer sizes + #--------------------------------------------------------------- + + def encode_int6(self, value): + """encodes 6-bit integer -> single hash64 character""" + if value < 0 or value > 63: + raise ValueError("value out of range") + if PY3: + return self.bytemap[value:value+1] + else: + return self._encode64(value) + + def encode_int12(self, value): + """encodes 12-bit integer -> 2 char string""" + if value < 0 or value > 0xFFF: + raise ValueError("value out of range") + raw = [value & 0x3f, (value>>6) & 0x3f] + if self.big: + raw = reversed(raw) + return join_byte_elems(imap(self._encode64, raw)) + + def encode_int24(self, value): + """encodes 24-bit integer -> 4 char string""" + if value < 0 or value > 0xFFFFFF: + raise ValueError("value out of range") + raw = [value & 0x3f, (value>>6) & 0x3f, + (value>>12) & 0x3f, (value>>18) & 0x3f] + if self.big: + raw = reversed(raw) + return join_byte_elems(imap(self._encode64, raw)) + + def encode_int30(self, value): + """decode 5 char string -> 30 bit integer""" + if value < 0 or value > 0x3fffffff: + raise ValueError("value out of range") + return self._encode_int(value, 30) + + def encode_int64(self, value): + """encode 64-bit integer -> 11 char hash64 string + + this format is used primarily by des-crypt & variants to encode + the DES output value used as a checksum. + """ + if value < 0 or value > 0xffffffffffffffff: + raise ValueError("value out of range") + return self._encode_int(value, 64) + + #=================================================================== + # eof + #=================================================================== + +class LazyBase64Engine(Base64Engine): + """Base64Engine which delays initialization until it's accessed""" + _lazy_opts = None + + def __init__(self, *args, **kwds): + self._lazy_opts = (args, kwds) + + def _lazy_init(self): + args, kwds = self._lazy_opts + super(LazyBase64Engine, self).__init__(*args, **kwds) + del self._lazy_opts + self.__class__ = Base64Engine + + def __getattribute__(self, attr): + if not attr.startswith("_"): + self._lazy_init() + return object.__getattribute__(self, attr) + +#------------------------------------------------------------- +# common variants +#------------------------------------------------------------- + +h64 = LazyBase64Engine(HASH64_CHARS) +h64big = LazyBase64Engine(HASH64_CHARS, big=True) +bcrypt64 = LazyBase64Engine(BCRYPT_CHARS, big=True) + +#============================================================================= +# eof +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/utils/decor.py b/venv/Lib/site-packages/passlib/utils/decor.py new file mode 100644 index 0000000..9041d5d --- /dev/null +++ b/venv/Lib/site-packages/passlib/utils/decor.py @@ -0,0 +1,233 @@ +""" +passlib.utils.decor -- helper decorators & properties +""" +#============================================================================= +# imports +#============================================================================= +# core +from __future__ import absolute_import, division, print_function +import logging +log = logging.getLogger(__name__) +from functools import wraps, update_wrapper +import types +from warnings import warn +# site +# pkg +from passlib.utils.compat import PY3 +# local +__all__ = [ + "classproperty", + "hybrid_method", + + "memoize_single_value", + "memoized_property", + + "deprecated_function", + "deprecated_method", +] + +#============================================================================= +# class-level decorators +#============================================================================= +class classproperty(object): + """Function decorator which acts like a combination of classmethod+property (limited to read-only properties)""" + + def __init__(self, func): + self.im_func = func + + def __get__(self, obj, cls): + return self.im_func(cls) + + @property + def __func__(self): + """py3 compatible alias""" + return self.im_func + +class hybrid_method(object): + """ + decorator which invokes function with class if called as class method, + and with object if called at instance level. + """ + + def __init__(self, func): + self.func = func + update_wrapper(self, func) + + def __get__(self, obj, cls): + if obj is None: + obj = cls + if PY3: + return types.MethodType(self.func, obj) + else: + return types.MethodType(self.func, obj, cls) + +#============================================================================= +# memoization +#============================================================================= + +def memoize_single_value(func): + """ + decorator for function which takes no args, + and memoizes result. exposes a ``.clear_cache`` method + to clear the cached value. + """ + cache = {} + + @wraps(func) + def wrapper(): + try: + return cache[True] + except KeyError: + pass + value = cache[True] = func() + return value + + def clear_cache(): + cache.pop(True, None) + wrapper.clear_cache = clear_cache + + return wrapper + +class memoized_property(object): + """ + decorator which invokes method once, then replaces attr with result + """ + def __init__(self, func): + self.__func__ = func + self.__name__ = func.__name__ + self.__doc__ = func.__doc__ + + def __get__(self, obj, cls): + if obj is None: + return self + value = self.__func__(obj) + setattr(obj, self.__name__, value) + return value + + if not PY3: + + @property + def im_func(self): + """py2 alias""" + return self.__func__ + + def clear_cache(self, obj): + """ + class-level helper to clear stored value (if any). + + usage: :samp:`type(self).{attr}.clear_cache(self)` + """ + obj.__dict__.pop(self.__name__, None) + + def peek_cache(self, obj, default=None): + """ + class-level helper to peek at stored value + + usage: :samp:`value = type(self).{attr}.clear_cache(self)` + """ + return obj.__dict__.get(self.__name__, default) + +# works but not used +##class memoized_class_property(object): +## """function decorator which calls function as classmethod, +## and replaces itself with result for current and all future invocations. +## """ +## def __init__(self, func): +## self.im_func = func +## +## def __get__(self, obj, cls): +## func = self.im_func +## value = func(cls) +## setattr(cls, func.__name__, value) +## return value +## +## @property +## def __func__(self): +## "py3 compatible alias" + +#============================================================================= +# deprecation +#============================================================================= +def deprecated_function(msg=None, deprecated=None, removed=None, updoc=True, + replacement=None, _is_method=False, + func_module=None): + """decorator to deprecate a function. + + :arg msg: optional msg, default chosen if omitted + :kwd deprecated: version when function was first deprecated + :kwd removed: version when function will be removed + :kwd replacement: alternate name / instructions for replacing this function. + :kwd updoc: add notice to docstring (default ``True``) + """ + if msg is None: + if _is_method: + msg = "the method %(mod)s.%(klass)s.%(name)s() is deprecated" + else: + msg = "the function %(mod)s.%(name)s() is deprecated" + if deprecated: + msg += " as of Passlib %(deprecated)s" + if removed: + msg += ", and will be removed in Passlib %(removed)s" + if replacement: + msg += ", use %s instead" % replacement + msg += "." + def build(func): + is_classmethod = _is_method and isinstance(func, classmethod) + if is_classmethod: + # NOTE: PY26 doesn't support "classmethod().__func__" directly... + func = func.__get__(None, type).__func__ + opts = dict( + mod=func_module or func.__module__, + name=func.__name__, + deprecated=deprecated, + removed=removed, + ) + if _is_method: + def wrapper(*args, **kwds): + tmp = opts.copy() + klass = args[0] if is_classmethod else args[0].__class__ + tmp.update(klass=klass.__name__, mod=klass.__module__) + warn(msg % tmp, DeprecationWarning, stacklevel=2) + return func(*args, **kwds) + else: + text = msg % opts + def wrapper(*args, **kwds): + warn(text, DeprecationWarning, stacklevel=2) + return func(*args, **kwds) + update_wrapper(wrapper, func) + if updoc and (deprecated or removed) and \ + wrapper.__doc__ and ".. deprecated::" not in wrapper.__doc__: + txt = deprecated or '' + if removed or replacement: + txt += "\n " + if removed: + txt += "and will be removed in version %s" % (removed,) + if replacement: + if removed: + txt += ", " + txt += "use %s instead" % replacement + txt += "." + if not wrapper.__doc__.strip(" ").endswith("\n"): + wrapper.__doc__ += "\n" + wrapper.__doc__ += "\n.. deprecated:: %s\n" % (txt,) + if is_classmethod: + wrapper = classmethod(wrapper) + return wrapper + return build + +def deprecated_method(msg=None, deprecated=None, removed=None, updoc=True, + replacement=None): + """decorator to deprecate a method. + + :arg msg: optional msg, default chosen if omitted + :kwd deprecated: version when method was first deprecated + :kwd removed: version when method will be removed + :kwd replacement: alternate name / instructions for replacing this method. + :kwd updoc: add notice to docstring (default ``True``) + """ + return deprecated_function(msg, deprecated, removed, updoc, replacement, + _is_method=True) + +#============================================================================= +# eof +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/utils/des.py b/venv/Lib/site-packages/passlib/utils/des.py new file mode 100644 index 0000000..034bfc4 --- /dev/null +++ b/venv/Lib/site-packages/passlib/utils/des.py @@ -0,0 +1,46 @@ +""" +passlib.utils.des - DEPRECATED LOCATION, WILL BE REMOVED IN 2.0 + +This has been moved to :mod:`passlib.crypto.des`. +""" +#============================================================================= +# import from new location +#============================================================================= +from warnings import warn +warn("the 'passlib.utils.des' module has been relocated to 'passlib.crypto.des' " + "as of passlib 1.7, and the old location will be removed in passlib 2.0", + DeprecationWarning) + +#============================================================================= +# relocated functions +#============================================================================= +from passlib.utils.decor import deprecated_function +from passlib.crypto.des import expand_des_key, des_encrypt_block, des_encrypt_int_block + +expand_des_key = deprecated_function(deprecated="1.7", removed="1.8", + replacement="passlib.crypto.des.expand_des_key")(expand_des_key) + +des_encrypt_block = deprecated_function(deprecated="1.7", removed="1.8", + replacement="passlib.crypto.des.des_encrypt_block")(des_encrypt_block) + +des_encrypt_int_block = deprecated_function(deprecated="1.7", removed="1.8", + replacement="passlib.crypto.des.des_encrypt_int_block")(des_encrypt_int_block) + +#============================================================================= +# deprecated functions -- not carried over to passlib.crypto.des +#============================================================================= +import struct +_unpack_uint64 = struct.Struct(">Q").unpack + +@deprecated_function(deprecated="1.6", removed="1.8", + replacement="passlib.crypto.des.des_encrypt_int_block()") +def mdes_encrypt_int_block(key, input, salt=0, rounds=1): # pragma: no cover -- deprecated & unused + if isinstance(key, bytes): + if len(key) == 7: + key = expand_des_key(key) + key = _unpack_uint64(key)[0] + return des_encrypt_int_block(key, input, salt, rounds) + +#============================================================================= +# eof +#============================================================================= diff --git a/venv/Lib/site-packages/passlib/utils/handlers.py b/venv/Lib/site-packages/passlib/utils/handlers.py new file mode 100644 index 0000000..f8681fa --- /dev/null +++ b/venv/Lib/site-packages/passlib/utils/handlers.py @@ -0,0 +1,2711 @@ +"""passlib.handler - code for implementing handlers, and global registry for handlers""" +#============================================================================= +# imports +#============================================================================= +from __future__ import with_statement +# core +import inspect +import logging; log = logging.getLogger(__name__) +import math +import threading +from warnings import warn +# site +# pkg +import passlib.exc as exc, passlib.ifc as ifc +from passlib.exc import MissingBackendError, PasslibConfigWarning, \ + PasslibHashWarning +from passlib.ifc import PasswordHash +from passlib.registry import get_crypt_handler +from passlib.utils import ( + consteq, getrandstr, getrandbytes, + rng, to_native_str, + is_crypt_handler, to_unicode, + MAX_PASSWORD_SIZE, accepts_keyword, as_bool, + update_mixin_classes) +from passlib.utils.binary import ( + BASE64_CHARS, HASH64_CHARS, PADDED_BASE64_CHARS, + HEX_CHARS, UPPER_HEX_CHARS, LOWER_HEX_CHARS, + ALL_BYTE_VALUES, +) +from passlib.utils.compat import join_byte_values, irange, u, native_string_types, \ + uascii_to_str, join_unicode, unicode, str_to_uascii, \ + join_unicode, unicode_or_bytes_types, PY2, int_types +from passlib.utils.decor import classproperty, deprecated_method +# local +__all__ = [ + # helpers for implementing MCF handlers + 'parse_mc2', + 'parse_mc3', + 'render_mc2', + 'render_mc3', + + # framework for implementing handlers + 'GenericHandler', + 'StaticHandler', + 'HasUserContext', + 'HasRawChecksum', + 'HasManyIdents', + 'HasSalt', + 'HasRawSalt', + 'HasRounds', + 'HasManyBackends', + + # other helpers + 'PrefixWrapper', + + # TODO: a bunch of other things are commonly assumed in this namespace + # (e.g. HEX_CHARS etc); need to audit uses and update this list. +] + +#============================================================================= +# constants +#============================================================================= + +# deprecated aliases - will be removed after passlib 1.8 +H64_CHARS = HASH64_CHARS +B64_CHARS = BASE64_CHARS +PADDED_B64_CHARS = PADDED_BASE64_CHARS +UC_HEX_CHARS = UPPER_HEX_CHARS +LC_HEX_CHARS = LOWER_HEX_CHARS + +#============================================================================= +# support functions +#============================================================================= +def _bitsize(count, chars): + """helper for bitsize() methods""" + if chars and count: + import math + return int(count * math.log(len(chars), 2)) + else: + return 0 + +def guess_app_stacklevel(start=1): + """ + try to guess stacklevel for application warning. + looks for first frame not part of passlib. + """ + frame = inspect.currentframe() + count = -start + try: + while frame: + name = frame.f_globals.get('__name__', "") + if name.startswith("passlib.tests.") or not name.startswith("passlib."): + return max(1, count) + count += 1 + frame = frame.f_back + return start + finally: + del frame + +def warn_hash_settings_deprecation(handler, kwds): + warn("passing settings to %(handler)s.hash() is deprecated, and won't be supported in Passlib 2.0; " + "use '%(handler)s.using(**settings).hash(secret)' instead" % dict(handler=handler.name), + DeprecationWarning, stacklevel=guess_app_stacklevel(2)) + +def extract_settings_kwds(handler, kwds): + """ + helper to extract settings kwds from mix of context & settings kwds. + pops settings keys from kwds, returns them as a dict. + """ + context_keys = set(handler.context_kwds) + return dict((key, kwds.pop(key)) for key in list(kwds) if key not in context_keys) + +#============================================================================= +# parsing helpers +#============================================================================= +_UDOLLAR = u("$") +_UZERO = u("0") + +def validate_secret(secret): + """ensure secret has correct type & size""" + if not isinstance(secret, unicode_or_bytes_types): + raise exc.ExpectedStringError(secret, "secret") + if len(secret) > MAX_PASSWORD_SIZE: + raise exc.PasswordSizeError(MAX_PASSWORD_SIZE) + +def to_unicode_for_identify(hash): + """convert hash to unicode for identify method""" + if isinstance(hash, unicode): + return hash + elif isinstance(hash, bytes): + # try as utf-8, but if it fails, use foolproof latin-1, + # since we don't really care about non-ascii chars + # when running identify. + try: + return hash.decode("utf-8") + except UnicodeDecodeError: + return hash.decode("latin-1") + else: + raise exc.ExpectedStringError(hash, "hash") + +def parse_mc2(hash, prefix, sep=_UDOLLAR, handler=None): + """parse hash using 2-part modular crypt format. + + this expects a hash of the format :samp:`{prefix}{salt}[${checksum}]`, + such as md5_crypt, and parses it into salt / checksum portions. + + :arg hash: the hash to parse (bytes or unicode) + :arg prefix: the identifying prefix (unicode) + :param sep: field separator (unicode, defaults to ``$``). + :param handler: handler class to pass to error constructors. + + :returns: + a ``(salt, chk | None)`` tuple. + """ + # detect prefix + hash = to_unicode(hash, "ascii", "hash") + assert isinstance(prefix, unicode) + if not hash.startswith(prefix): + raise exc.InvalidHashError(handler) + + # parse 2-part hash or 1-part config string + assert isinstance(sep, unicode) + parts = hash[len(prefix):].split(sep) + if len(parts) == 2: + salt, chk = parts + return salt, chk or None + elif len(parts) == 1: + return parts[0], None + else: + raise exc.MalformedHashError(handler) + +def parse_mc3(hash, prefix, sep=_UDOLLAR, rounds_base=10, + default_rounds=None, handler=None): + """parse hash using 3-part modular crypt format. + + this expects a hash of the format :samp:`{prefix}[{rounds}]${salt}[${checksum}]`, + such as sha1_crypt, and parses it into rounds / salt / checksum portions. + tries to convert the rounds to an integer, + and throws error if it has zero-padding. + + :arg hash: the hash to parse (bytes or unicode) + :arg prefix: the identifying prefix (unicode) + :param sep: field separator (unicode, defaults to ``$``). + :param rounds_base: + the numeric base the rounds are encoded in (defaults to base 10). + :param default_rounds: + the default rounds value to return if the rounds field was omitted. + if this is ``None`` (the default), the rounds field is *required*. + :param handler: handler class to pass to error constructors. + + :returns: + a ``(rounds : int, salt, chk | None)`` tuple. + """ + # detect prefix + hash = to_unicode(hash, "ascii", "hash") + assert isinstance(prefix, unicode) + if not hash.startswith(prefix): + raise exc.InvalidHashError(handler) + + # parse 3-part hash or 2-part config string + assert isinstance(sep, unicode) + parts = hash[len(prefix):].split(sep) + if len(parts) == 3: + rounds, salt, chk = parts + elif len(parts) == 2: + rounds, salt = parts + chk = None + else: + raise exc.MalformedHashError(handler) + + # validate & parse rounds portion + if rounds.startswith(_UZERO) and rounds != _UZERO: + raise exc.ZeroPaddedRoundsError(handler) + elif rounds: + rounds = int(rounds, rounds_base) + elif default_rounds is None: + raise exc.MalformedHashError(handler, "empty rounds field") + else: + rounds = default_rounds + + # return result + return rounds, salt, chk or None + +# def parse_mc3_long(hash, prefix, sep=_UDOLLAR, handler=None): +# """ +# parse hash using 3-part modular crypt format, +# with complex settings string instead of simple rounds. +# otherwise works same as :func:`parse_mc3` +# """ +# # detect prefix +# hash = to_unicode(hash, "ascii", "hash") +# assert isinstance(prefix, unicode) +# if not hash.startswith(prefix): +# raise exc.InvalidHashError(handler) +# +# # parse 3-part hash or 2-part config string +# assert isinstance(sep, unicode) +# parts = hash[len(prefix):].split(sep) +# if len(parts) == 3: +# return parts +# elif len(parts) == 2: +# settings, salt = parts +# return settings, salt, None +# else: +# raise exc.MalformedHashError(handler) + +def parse_int(source, base=10, default=None, param="value", handler=None): + """ + helper to parse an integer config field + + :arg source: unicode source string + :param base: numeric base + :param default: optional default if source is empty + :param param: name of variable, for error msgs + :param handler: handler class, for error msgs + """ + if source.startswith(_UZERO) and source != _UZERO: + raise exc.MalformedHashError(handler, "zero-padded %s field" % param) + elif source: + return int(source, base) + elif default is None: + raise exc.MalformedHashError(handler, "empty %s field" % param) + else: + return default + +#============================================================================= +# formatting helpers +#============================================================================= +def render_mc2(ident, salt, checksum, sep=u("$")): + """format hash using 2-part modular crypt format; inverse of parse_mc2() + + returns native string with format :samp:`{ident}{salt}[${checksum}]`, + such as used by md5_crypt. + + :arg ident: identifier prefix (unicode) + :arg salt: encoded salt (unicode) + :arg checksum: encoded checksum (unicode or None) + :param sep: separator char (unicode, defaults to ``$``) + + :returns: + config or hash (native str) + """ + if checksum: + parts = [ident, salt, sep, checksum] + else: + parts = [ident, salt] + return uascii_to_str(join_unicode(parts)) + +def render_mc3(ident, rounds, salt, checksum, sep=u("$"), rounds_base=10): + """format hash using 3-part modular crypt format; inverse of parse_mc3() + + returns native string with format :samp:`{ident}[{rounds}$]{salt}[${checksum}]`, + such as used by sha1_crypt. + + :arg ident: identifier prefix (unicode) + :arg rounds: rounds field (int or None) + :arg salt: encoded salt (unicode) + :arg checksum: encoded checksum (unicode or None) + :param sep: separator char (unicode, defaults to ``$``) + :param rounds_base: base to encode rounds value (defaults to base 10) + + :returns: + config or hash (native str) + """ + if rounds is None: + rounds = u('') + elif rounds_base == 16: + rounds = u("%x") % rounds + else: + assert rounds_base == 10 + rounds = unicode(rounds) + if checksum: + parts = [ident, rounds, sep, salt, sep, checksum] + else: + parts = [ident, rounds, sep, salt] + return uascii_to_str(join_unicode(parts)) + + +def mask_value(value, show=4, pct=0.125, char=u"*"): + """ + helper to mask contents of sensitive field. + + :param value: + raw value (str, bytes, etc) + + :param show: + max # of characters to remain visible + + :param pct: + don't show more than this % of input. + + :param char: + character to use for masking + + :rtype: str | None + """ + if value is None: + return None + if not isinstance(value, unicode): + if isinstance(value, bytes): + from passlib.utils.binary import ab64_encode + value = ab64_encode(value).decode("ascii") + else: + value = unicode(value) + size = len(value) + show = min(show, int(size * pct)) + return value[:show] + char * (size - show) + +#============================================================================= +# parameter helpers +#============================================================================= + +def validate_default_value(handler, default, norm, param="value"): + """ + assert helper that quickly validates default value. + designed to get out of the way and reduce overhead when asserts are stripped. + """ + assert default is not None, "%s lacks default %s" % (handler.name, param) + assert norm(default) == default, "%s: invalid default %s: %r" % (handler.name, param, default) + return True + +def norm_integer(handler, value, min=1, max=None, # * + param="value", relaxed=False): + """ + helper to normalize and validate an integer value (e.g. rounds, salt_size) + + :arg value: value provided to constructor + :arg default: default value if none provided. if set to ``None``, value is required. + :arg param: name of parameter (xxx: move to first arg?) + :param min: minimum value (defaults to 1) + :param max: maximum value (default ``None`` means no maximum) + :returns: validated value + """ + # check type + if not isinstance(value, int_types): + raise exc.ExpectedTypeError(value, "integer", param) + + # check minimum + if value < min: + msg = "%s: %s (%d) is too low, must be at least %d" % (handler.name, param, value, min) + if relaxed: + warn(msg, exc.PasslibHashWarning) + value = min + else: + raise ValueError(msg) + + # check maximum + if max and value > max: + msg = "%s: %s (%d) is too large, cannot be more than %d" % (handler.name, param, value, max) + if relaxed: + warn(msg, exc.PasslibHashWarning) + value = max + else: + raise ValueError(msg) + + return value + +#============================================================================= +# MinimalHandler +#============================================================================= +class MinimalHandler(PasswordHash): + """ + helper class for implementing hash handlers. + provides nothing besides a base implementation of the .using() subclass constructor. + """ + #=================================================================== + # class attr + #=================================================================== + + #: private flag used by using() constructor to detect if this is already a subclass. + _configured = False + + #=================================================================== + # configuration interface + #=================================================================== + + @classmethod + def using(cls, relaxed=False): + # NOTE: this provides the base implementation, which takes care of + # creating the newly configured class. Mixins and subclasses + # should wrap this, and modify the returned class to suit their options. + # NOTE: 'relaxed' keyword is ignored here, but parsed so that subclasses + # can check for it as argument, and modify their parsing behavior accordingly. + name = cls.__name__ + if not cls._configured: + # TODO: straighten out class naming, repr, and .name attr + name = "" % name + return type(name, (cls,), dict(__module__=cls.__module__, _configured=True)) + + #=================================================================== + # eoc + #=================================================================== + +class TruncateMixin(MinimalHandler): + """ + PasswordHash mixin which provides a method + that will check if secret would be truncated, + and can be configured to throw an error. + + .. warning:: + + Hashers using this mixin will generally need to override + the default PasswordHash.truncate_error policy of "True", + and will similarly want to override .truncate_verify_reject as well. + + TODO: This should be done explicitly, but for now this mixin sets + these flags implicitly. + """ + + truncate_error = False + truncate_verify_reject = False + + @classmethod + def using(cls, truncate_error=None, **kwds): + subcls = super(TruncateMixin, cls).using(**kwds) + if truncate_error is not None: + truncate_error = as_bool(truncate_error, param="truncate_error") + if truncate_error is not None: + subcls.truncate_error = truncate_error + return subcls + + @classmethod + def _check_truncate_policy(cls, secret): + """ + make sure secret won't be truncated. + NOTE: this should only be called for .hash(), not for .verify(), + which should honor the .truncate_verify_reject policy. + """ + assert cls.truncate_size is not None, "truncate_size must be set by subclass" + if cls.truncate_error and len(secret) > cls.truncate_size: + raise exc.PasswordTruncateError(cls) + +#============================================================================= +# GenericHandler +#============================================================================= +class GenericHandler(MinimalHandler): + """helper class for implementing hash handlers. + + GenericHandler-derived classes will have (at least) the following + constructor options, though others may be added by mixins + and by the class itself: + + :param checksum: + this should contain the digest portion of a + parsed hash (mainly provided when the constructor is called + by :meth:`from_string()`). + defaults to ``None``. + + :param use_defaults: + If ``False`` (the default), a :exc:`TypeError` should be thrown + if any settings required by the handler were not explicitly provided. + + If ``True``, the handler should attempt to provide a default for any + missing values. This means generate missing salts, fill in default + cost parameters, etc. + + This is typically only set to ``True`` when the constructor + is called by :meth:`hash`, allowing user-provided values + to be handled in a more permissive manner. + + :param relaxed: + If ``False`` (the default), a :exc:`ValueError` should be thrown + if any settings are out of bounds or otherwise invalid. + + If ``True``, they should be corrected if possible, and a warning + issue. If not possible, only then should an error be raised. + (e.g. under ``relaxed=True``, rounds values will be clamped + to min/max rounds). + + This is mainly used when parsing the config strings of certain + hashes, whose specifications implementations to be tolerant + of incorrect values in salt strings. + + Class Attributes + ================ + + .. attribute:: ident + + [optional] + If this attribute is filled in, the default :meth:`identify` method will use + it as a identifying prefix that can be used to recognize instances of this handler's + hash. Filling this out is recommended for speed. + + This should be a unicode str. + + .. attribute:: _hash_regex + + [optional] + If this attribute is filled in, the default :meth:`identify` method + will use it to recognize instances of the hash. If :attr:`ident` + is specified, this will be ignored. + + This should be a unique regex object. + + .. attribute:: checksum_size + + [optional] + Specifies the number of characters that should be expected in the checksum string. + If omitted, no check will be performed. + + .. attribute:: checksum_chars + + [optional] + A string listing all the characters allowed in the checksum string. + If omitted, no check will be performed. + + This should be a unicode str. + + .. attribute:: _stub_checksum + + Placeholder checksum that will be used by genconfig() + in lieu of actually generating a hash for the empty string. + This should be a string of the same datatype as :attr:`checksum`. + + Instance Attributes + =================== + .. attribute:: checksum + + The checksum string provided to the constructor (after passing it + through :meth:`_norm_checksum`). + + Required Subclass Methods + ========================= + The following methods must be provided by handler subclass: + + .. automethod:: from_string + .. automethod:: to_string + .. automethod:: _calc_checksum + + Default Methods + =============== + The following methods have default implementations that should work for + most cases, though they may be overridden if the hash subclass needs to: + + .. automethod:: _norm_checksum + + .. automethod:: genconfig + .. automethod:: genhash + .. automethod:: identify + .. automethod:: hash + .. automethod:: verify + """ + + #=================================================================== + # class attr + #=================================================================== + # this must be provided by the actual class. + setting_kwds = None + + # providing default since most classes don't use this at all. + context_kwds = () + + # optional prefix that uniquely identifies hash + ident = None + + # optional regexp for recognizing hashes, + # used by default identify() if .ident isn't specified. + _hash_regex = None + + # if specified, _norm_checksum will require this length + checksum_size = None + + # if specified, _norm_checksum() will validate this + checksum_chars = None + + # private flag used by HasRawChecksum + _checksum_is_bytes = False + + #=================================================================== + # instance attrs + #=================================================================== + checksum = None # stores checksum +# use_defaults = False # whether _norm_xxx() funcs should fill in defaults. +# relaxed = False # when _norm_xxx() funcs should be strict about inputs + + #=================================================================== + # init + #=================================================================== + def __init__(self, checksum=None, use_defaults=False, **kwds): + self.use_defaults = use_defaults + super(GenericHandler, self).__init__(**kwds) + if checksum is not None: + # XXX: do we need to set .relaxed for checksum coercion? + self.checksum = self._norm_checksum(checksum) + + # NOTE: would like to make this classmethod, but fshp checksum size + # is dependant on .variant, so leaving this as instance method. + def _norm_checksum(self, checksum, relaxed=False): + """validates checksum keyword against class requirements, + returns normalized version of checksum. + """ + # NOTE: by default this code assumes checksum should be unicode. + # For classes where the checksum is raw bytes, the HasRawChecksum sets + # the _checksum_is_bytes flag which alters various code paths below. + + # normalize to bytes / unicode + raw = self._checksum_is_bytes + if raw: + # NOTE: no clear route to reasonably convert unicode -> raw bytes, + # so 'relaxed' does nothing here + if not isinstance(checksum, bytes): + raise exc.ExpectedTypeError(checksum, "bytes", "checksum") + + elif not isinstance(checksum, unicode): + if isinstance(checksum, bytes) and relaxed: + warn("checksum should be unicode, not bytes", PasslibHashWarning) + checksum = checksum.decode("ascii") + else: + raise exc.ExpectedTypeError(checksum, "unicode", "checksum") + + # check size + cc = self.checksum_size + if cc and len(checksum) != cc: + raise exc.ChecksumSizeError(self, raw=raw) + + # check charset + if not raw: + cs = self.checksum_chars + if cs and any(c not in cs for c in checksum): + raise ValueError("invalid characters in %s checksum" % (self.name,)) + + return checksum + + #=================================================================== + # password hash api - formatting interface + #=================================================================== + @classmethod + def identify(cls, hash): + # NOTE: subclasses may wish to use faster / simpler identify, + # and raise value errors only when an invalid (but identifiable) + # string is parsed + hash = to_unicode_for_identify(hash) + if not hash: + return False + + # does class specify a known unique prefix to look for? + ident = cls.ident + if ident is not None: + return hash.startswith(ident) + + # does class provide a regexp to use? + pat = cls._hash_regex + if pat is not None: + return pat.match(hash) is not None + + # as fallback, try to parse hash, and see if we succeed. + # inefficient, but works for most cases. + try: + cls.from_string(hash) + return True + except ValueError: + return False + + @classmethod + def from_string(cls, hash, **context): # pragma: no cover + r""" + return parsed instance from hash/configuration string + + :param \\*\\*context: + context keywords to pass to constructor (if applicable). + + :raises ValueError: if hash is incorrectly formatted + + :returns: + hash parsed into components, + for formatting / calculating checksum. + """ + raise NotImplementedError("%s must implement from_string()" % (cls,)) + + def to_string(self): # pragma: no cover + """render instance to hash or configuration string + + :returns: + hash string with salt & digest included. + + should return native string type (ascii-bytes under python 2, + unicode under python 3) + """ + raise NotImplementedError("%s must implement from_string()" % (self.__class__,)) + + #=================================================================== + # checksum generation + #=================================================================== + + # NOTE: this is only used by genconfig(), and will be removed in passlib 2.0 + @property + def _stub_checksum(self): + """ + placeholder used by default .genconfig() so it can avoid expense of calculating digest. + """ + # used fixed string if available + if self.checksum_size: + if self._checksum_is_bytes: + return b'\x00' * self.checksum_size + if self.checksum_chars: + return self.checksum_chars[0] * self.checksum_size + + # hack to minimize cost of calculating real checksum + if isinstance(self, HasRounds): + orig = self.rounds + self.rounds = self.min_rounds or 1 + try: + return self._calc_checksum("") + finally: + self.rounds = orig + + # final fallback, generate a real checksum + return self._calc_checksum("") + + def _calc_checksum(self, secret): # pragma: no cover + """given secret; calcuate and return encoded checksum portion of hash + string, taking config from object state + + calc checksum implementations may assume secret is always + either unicode or bytes, checks are performed by verify/etc. + """ + raise NotImplementedError("%s must implement _calc_checksum()" % + (self.__class__,)) + + #=================================================================== + #'application' interface (default implementation) + #=================================================================== + + @classmethod + def hash(cls, secret, **kwds): + if kwds: + # Deprecating passing any settings keywords via .hash() as of passlib 1.7; everything + # should use .using().hash() instead. If any keywords are specified, presume they're + # context keywords by default (the common case), and extract out any settings kwds. + # Support for passing settings via .hash() will be removed in Passlib 2.0, along with + # this block of code. + settings = extract_settings_kwds(cls, kwds) + if settings: + warn_hash_settings_deprecation(cls, settings) + return cls.using(**settings).hash(secret, **kwds) + # NOTE: at this point, 'kwds' should just contain context_kwds subset + validate_secret(secret) + self = cls(use_defaults=True, **kwds) + self.checksum = self._calc_checksum(secret) + return self.to_string() + + @classmethod + def verify(cls, secret, hash, **context): + # NOTE: classes with multiple checksum encodings should either + # override this method, or ensure that from_string() / _norm_checksum() + # ensures .checksum always uses a single canonical representation. + validate_secret(secret) + self = cls.from_string(hash, **context) + chk = self.checksum + if chk is None: + raise exc.MissingDigestError(cls) + return consteq(self._calc_checksum(secret), chk) + + #=================================================================== + # legacy crypt interface + #=================================================================== + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genconfig(cls, **kwds): + # NOTE: 'kwds' should generally always be settings, so after this completes, *should* be empty. + settings = extract_settings_kwds(cls, kwds) + if settings: + return cls.using(**settings).genconfig(**kwds) + # NOTE: this uses optional stub checksum to bypass potentially expensive digest generation, + # when caller just wants the config string. + self = cls(use_defaults=True, **kwds) + self.checksum = self._stub_checksum + return self.to_string() + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, **context): + if config is None: + raise TypeError("config must be string") + validate_secret(secret) + self = cls.from_string(config, **context) + self.checksum = self._calc_checksum(secret) + return self.to_string() + + #=================================================================== + # migration interface (basde implementation) + #=================================================================== + + @classmethod + def needs_update(cls, hash, secret=None, **kwds): + # NOTE: subclasses should generally just wrap _calc_needs_update() + # to check their particular keywords. + self = cls.from_string(hash) + assert isinstance(self, cls) + return self._calc_needs_update(secret=secret, **kwds) + + def _calc_needs_update(self, secret=None): + """ + internal helper for :meth:`needs_update`. + """ + # NOTE: this just provides a stub, subclasses & mixins + # should override this with their own tests. + return False + + #=================================================================== + # experimental - the following methods are not finished or tested, + # but way work correctly for some hashes + #=================================================================== + + #: internal helper for forcing settings to be included, even if default matches + _always_parse_settings = () + + #: internal helper for excluding certain setting_kwds from parsehash() result + _unparsed_settings = ("salt_size", "relaxed") + + #: parsehash() keys that need to be sanitized + _unsafe_settings = ("salt", "checksum") + + @classproperty + def _parsed_settings(cls): + """ + helper for :meth:`parsehash` -- + returns list of attributes which should be extracted by parse_hash() from hasher object. + + default implementation just takes setting_kwds, and excludes _unparsed_settings + """ + return tuple(key for key in cls.setting_kwds if key not in cls._unparsed_settings) + + @classmethod + def parsehash(cls, hash, checksum=True, sanitize=False): + """[experimental method] parse hash into dictionary of settings. + + this essentially acts as the inverse of :meth:`hash`: for most + cases, if ``hash = cls.hash(secret, **opts)``, then + ``cls.parsehash(hash)`` will return a dict matching the original options + (with the extra keyword *checksum*). + + this method may not work correctly for all hashes, + and may not be available on some few. its interface may + change in future releases, if it's kept around at all. + + :arg hash: hash to parse + :param checksum: include checksum keyword? (defaults to True) + :param sanitize: mask data for sensitive fields? (defaults to False) + """ + # FIXME: this may not work for hashes with non-standard settings. + # XXX: how should this handle checksum/salt encoding? + # need to work that out for hash() anyways. + self = cls.from_string(hash) + # XXX: could split next few lines out as self._parsehash() for subclassing + # XXX: could try to resolve ident/variant to publically suitable alias. + # XXX: for v1.8, consider making "always" the default policy, and compare to class default + # only for whitelisted attrs? or make this whole method obsolete by reworking + # so "hasher" object & it's attrs are public? + UNSET = object() + always = self._always_parse_settings + kwds = dict((key, getattr(self, key)) for key in self._parsed_settings + if key in always or getattr(self, key) != getattr(cls, key, UNSET)) + if checksum and self.checksum is not None: + kwds['checksum'] = self.checksum + if sanitize: + if sanitize is True: + sanitize = mask_value + for key in cls._unsafe_settings: + if key in kwds: + kwds[key] = sanitize(kwds[key]) + return kwds + + @classmethod + def bitsize(cls, **kwds): + """[experimental method] return info about bitsizes of hash""" + try: + info = super(GenericHandler, cls).bitsize(**kwds) + except AttributeError: + info = {} + cc = ALL_BYTE_VALUES if cls._checksum_is_bytes else cls.checksum_chars + if cls.checksum_size and cc: + # FIXME: this may overestimate size due to padding bits (e.g. bcrypt) + # FIXME: this will be off by 1 for case-insensitive hashes. + info['checksum'] = _bitsize(cls.checksum_size, cc) + return info + + #=================================================================== + # eoc + #=================================================================== + +class StaticHandler(GenericHandler): + """GenericHandler mixin for classes which have no settings. + + This mixin assumes the entirety of the hash ise stored in the + :attr:`checksum` attribute; that the hash has no rounds, salt, + etc. This class provides the following: + + * a default :meth:`genconfig` that always returns None. + * a default :meth:`from_string` and :meth:`to_string` + that store the entire hash within :attr:`checksum`, + after optionally stripping a constant prefix. + + All that is required by subclasses is an implementation of + the :meth:`_calc_checksum` method. + """ + # TODO: document _norm_hash() + + setting_kwds = () + + # optional constant prefix subclasses can specify + _hash_prefix = u("") + + @classmethod + def from_string(cls, hash, **context): + # default from_string() which strips optional prefix, + # and passes rest unchanged as checksum value. + hash = to_unicode(hash, "ascii", "hash") + hash = cls._norm_hash(hash) + # could enable this for extra strictness + ##pat = cls._hash_regex + ##if pat and pat.match(hash) is None: + ## raise ValueError("not a valid %s hash" % (cls.name,)) + prefix = cls._hash_prefix + if prefix: + if hash.startswith(prefix): + hash = hash[len(prefix):] + else: + raise exc.InvalidHashError(cls) + return cls(checksum=hash, **context) + + @classmethod + def _norm_hash(cls, hash): + """helper for subclasses to normalize case if needed""" + return hash + + def to_string(self): + return uascii_to_str(self._hash_prefix + self.checksum) + + # per-subclass: stores dynamically created subclass used by _calc_checksum() stub + __cc_compat_hack = None + + def _calc_checksum(self, secret): + """given secret; calcuate and return encoded checksum portion of hash + string, taking config from object state + """ + # NOTE: prior to 1.6, StaticHandler required classes implement genhash + # instead of this method. so if we reach here, we try calling genhash. + # if that succeeds, we issue deprecation warning. if it fails, + # we'll just recurse back to here, but in a different instance. + # so before we call genhash, we create a subclass which handles + # throwing the NotImplementedError. + cls = self.__class__ + assert cls.__module__ != __name__ + wrapper_cls = cls.__cc_compat_hack + if wrapper_cls is None: + def inner(self, secret): + raise NotImplementedError("%s must implement _calc_checksum()" % + (cls,)) + wrapper_cls = cls.__cc_compat_hack = type(cls.__name__ + "_wrapper", + (cls,), dict(_calc_checksum=inner, __module__=cls.__module__)) + context = dict((k,getattr(self,k)) for k in self.context_kwds) + # NOTE: passing 'config=None' here even though not currently allowed by ifc, + # since it *is* allowed under the old 1.5 ifc we're checking for here. + try: + hash = wrapper_cls.genhash(secret, None, **context) + except TypeError as err: + if str(err) == "config must be string": + raise NotImplementedError("%s must implement _calc_checksum()" % + (cls,)) + else: + raise + warn("%r should be updated to implement StaticHandler._calc_checksum() " + "instead of StaticHandler.genhash(), support for the latter " + "style will be removed in Passlib 1.8" % cls, + DeprecationWarning) + return str_to_uascii(hash) + +#============================================================================= +# GenericHandler mixin classes +#============================================================================= +class HasEncodingContext(GenericHandler): + """helper for classes which require knowledge of the encoding used""" + context_kwds = ("encoding",) + default_encoding = "utf-8" + + def __init__(self, encoding=None, **kwds): + super(HasEncodingContext, self).__init__(**kwds) + self.encoding = encoding or self.default_encoding + +class HasUserContext(GenericHandler): + """helper for classes which require a user context keyword""" + context_kwds = ("user",) + + def __init__(self, user=None, **kwds): + super(HasUserContext, self).__init__(**kwds) + self.user = user + + # XXX: would like to validate user input here, but calls to from_string() + # which lack context keywords would then fail; so leaving code per-handler. + + # wrap funcs to accept 'user' as positional arg for ease of use. + @classmethod + def hash(cls, secret, user=None, **context): + return super(HasUserContext, cls).hash(secret, user=user, **context) + + @classmethod + def verify(cls, secret, hash, user=None, **context): + return super(HasUserContext, cls).verify(secret, hash, user=user, **context) + + @deprecated_method(deprecated="1.7", removed="2.0") + @classmethod + def genhash(cls, secret, config, user=None, **context): + return super(HasUserContext, cls).genhash(secret, config, user=user, **context) + + # XXX: how to guess the entropy of a username? + # most of these hashes are for a system (e.g. Oracle) + # which has a few *very common* names and thus really low entropy; + # while the rest are slightly less predictable. + # need to find good reference about this. + ##@classmethod + ##def bitsize(cls, **kwds): + ## info = super(HasUserContext, cls).bitsize(**kwds) + ## info['user'] = xxx + ## return info + +#------------------------------------------------------------------------ +# checksum mixins +#------------------------------------------------------------------------ +class HasRawChecksum(GenericHandler): + """mixin for classes which work with decoded checksum bytes + + .. todo:: + + document this class's usage + """ + # NOTE: GenericHandler.checksum_chars is ignored by this implementation. + + # NOTE: all HasRawChecksum code is currently part of GenericHandler, + # using private '_checksum_is_bytes' flag. + # this arrangement may be changed in the future. + _checksum_is_bytes = True + +#------------------------------------------------------------------------ +# ident mixins +#------------------------------------------------------------------------ +class HasManyIdents(GenericHandler): + """mixin for hashes which use multiple prefix identifiers + + For the hashes which may use multiple identifier prefixes, + this mixin adds an ``ident`` keyword to constructor. + Any value provided is passed through the :meth:`norm_idents` method, + which takes care of validating the identifier, + as well as allowing aliases for easier specification + of the identifiers by the user. + + .. todo:: + + document this class's usage + + Class Methods + ============= + .. todo:: document using() and needs_update() options + """ + + #=================================================================== + # class attrs + #=================================================================== + default_ident = None # should be unicode + ident_values = None # should be list of unicode strings + ident_aliases = None # should be dict of unicode -> unicode + # NOTE: any aliases provided to norm_ident() as bytes + # will have been converted to unicode before + # comparing against this dictionary. + + # NOTE: relying on test_06_HasManyIdents() to verify + # these are configured correctly. + + #=================================================================== + # instance attrs + #=================================================================== + ident = None + + #=================================================================== + # variant constructor + #=================================================================== + @classmethod + def using(cls, # keyword only... + default_ident=None, ident=None, **kwds): + """ + This mixin adds support for the following :meth:`~passlib.ifc.PasswordHash.using` keywords: + + :param default_ident: + default identifier that will be used by resulting customized hasher. + + :param ident: + supported as alternate alias for **default_ident**. + """ + # resolve aliases + if ident is not None: + if default_ident is not None: + raise TypeError("'default_ident' and 'ident' are mutually exclusive") + default_ident = ident + + # create subclass + subcls = super(HasManyIdents, cls).using(**kwds) + + # add custom default ident + # (NOTE: creates instance to run value through _norm_ident()) + if default_ident is not None: + subcls.default_ident = cls(ident=default_ident, use_defaults=True).ident + return subcls + + #=================================================================== + # init + #=================================================================== + def __init__(self, ident=None, **kwds): + super(HasManyIdents, self).__init__(**kwds) + + # init ident + if ident is not None: + ident = self._norm_ident(ident) + elif self.use_defaults: + ident = self.default_ident + assert validate_default_value(self, ident, self._norm_ident, param="default_ident") + else: + raise TypeError("no ident specified") + self.ident = ident + + @classmethod + def _norm_ident(cls, ident): + """ + helper which normalizes & validates 'ident' value. + """ + # handle bytes + assert ident is not None + if isinstance(ident, bytes): + ident = ident.decode('ascii') + + # check if identifier is valid + iv = cls.ident_values + if ident in iv: + return ident + + # resolve aliases, and recheck against ident_values + ia = cls.ident_aliases + if ia: + try: + value = ia[ident] + except KeyError: + pass + else: + if value in iv: + return value + + # failure! + # XXX: give this it's own error type? + raise ValueError("invalid ident: %r" % (ident,)) + + #=================================================================== + # password hash api + #=================================================================== + @classmethod + def identify(cls, hash): + hash = to_unicode_for_identify(hash) + return hash.startswith(cls.ident_values) + + @classmethod + def _parse_ident(cls, hash): + """extract ident prefix from hash, helper for subclasses' from_string()""" + hash = to_unicode(hash, "ascii", "hash") + for ident in cls.ident_values: + if hash.startswith(ident): + return ident, hash[len(ident):] + raise exc.InvalidHashError(cls) + + # XXX: implement a needs_update() helper that marks everything but default_ident as deprecated? + + #=================================================================== + # eoc + #=================================================================== + +#------------------------------------------------------------------------ +# salt mixins +#------------------------------------------------------------------------ +class HasSalt(GenericHandler): + """mixin for validating salts. + + This :class:`GenericHandler` mixin adds a ``salt`` keyword to the class constuctor; + any value provided is passed through the :meth:`_norm_salt` method, + which takes care of validating salt length and content, + as well as generating new salts if one it not provided. + + :param salt: + optional salt string + + :param salt_size: + optional size of salt (only used if no salt provided); + defaults to :attr:`default_salt_size`. + + Class Attributes + ================ + In order for :meth:`!_norm_salt` to do its job, the following + attributes should be provided by the handler subclass: + + .. attribute:: min_salt_size + + The minimum number of characters allowed in a salt string. + An :exc:`ValueError` will be throw if the provided salt is too small. + Defaults to ``0``. + + .. attribute:: max_salt_size + + The maximum number of characters allowed in a salt string. + By default an :exc:`ValueError` will be throw if the provided salt is + too large; but if ``relaxed=True``, it will be clipped and a warning + issued instead. Defaults to ``None``, for no maximum. + + .. attribute:: default_salt_size + + [required] + If no salt is provided, this should specify the size of the salt + that will be generated by :meth:`_generate_salt`. By default + this will fall back to :attr:`max_salt_size`. + + .. attribute:: salt_chars + + A string containing all the characters which are allowed in the salt + string. An :exc:`ValueError` will be throw if any other characters + are encountered. May be set to ``None`` to skip this check (but see + in :attr:`default_salt_chars`). + + .. attribute:: default_salt_chars + + [required] + This attribute controls the set of characters use to generate + *new* salt strings. By default, it mirrors :attr:`salt_chars`. + If :attr:`!salt_chars` is ``None``, this attribute must be specified + in order to generate new salts. Aside from that purpose, + the main use of this attribute is for hashes which wish to generate + salts from a restricted subset of :attr:`!salt_chars`; such as + accepting all characters, but only using a-z. + + Instance Attributes + =================== + .. attribute:: salt + + This instance attribute will be filled in with the salt provided + to the constructor (as adapted by :meth:`_norm_salt`) + + Subclassable Methods + ==================== + .. automethod:: _norm_salt + .. automethod:: _generate_salt + """ + # TODO: document _truncate_salt() + # XXX: allow providing raw salt to this class, and encoding it? + + #=================================================================== + # class attrs + #=================================================================== + + min_salt_size = 0 + max_salt_size = None + salt_chars = None + + @classproperty + def default_salt_size(cls): + """default salt size (defaults to *max_salt_size*)""" + return cls.max_salt_size + + @classproperty + def default_salt_chars(cls): + """charset used to generate new salt strings (defaults to *salt_chars*)""" + return cls.salt_chars + + # private helpers for HasRawSalt, shouldn't be used by subclasses + _salt_is_bytes = False + _salt_unit = "chars" + + # TODO: could support using(min/max_desired_salt_size) via using() and needs_update() + + #=================================================================== + # instance attrs + #=================================================================== + salt = None + + #=================================================================== + # variant constructor + #=================================================================== + @classmethod + def using(cls, # keyword only... + default_salt_size=None, + salt_size=None, # aliases used by CryptContext + salt=None, + **kwds): + + # check for aliases used by CryptContext + if salt_size is not None: + if default_salt_size is not None: + raise TypeError("'salt_size' and 'default_salt_size' aliases are mutually exclusive") + default_salt_size = salt_size + + # generate new subclass + subcls = super(HasSalt, cls).using(**kwds) + + # replace default_rounds + relaxed = kwds.get("relaxed") + if default_salt_size is not None: + if isinstance(default_salt_size, native_string_types): + default_salt_size = int(default_salt_size) + subcls.default_salt_size = subcls._clip_to_valid_salt_size(default_salt_size, + param="salt_size", + relaxed=relaxed) + + # if salt specified, replace _generate_salt() with fixed output. + # NOTE: this is mainly useful for testing / debugging. + if salt is not None: + salt = subcls._norm_salt(salt, relaxed=relaxed) + subcls._generate_salt = staticmethod(lambda: salt) + + return subcls + + # XXX: would like to combine w/ _norm_salt() code below, but doesn't quite fit. + @classmethod + def _clip_to_valid_salt_size(cls, salt_size, param="salt_size", relaxed=True): + """ + internal helper -- + clip salt size value to handler's absolute limits (min_salt_size / max_salt_size) + + :param relaxed: + if ``True`` (the default), issues PasslibHashWarning is rounds are outside allowed range. + if ``False``, raises a ValueError instead. + + :param param: + optional name of parameter to insert into error/warning messages. + + :returns: + clipped rounds value + """ + mn = cls.min_salt_size + mx = cls.max_salt_size + + # check if salt size is fixed + if mn == mx: + if salt_size != mn: + msg = "%s: %s (%d) must be exactly %d" % (cls.name, param, salt_size, mn) + if relaxed: + warn(msg, PasslibHashWarning) + else: + raise ValueError(msg) + return mn + + # check min size + if salt_size < mn: + msg = "%s: %s (%r) below min_salt_size (%d)" % (cls.name, param, salt_size, mn) + if relaxed: + warn(msg, PasslibHashWarning) + salt_size = mn + else: + raise ValueError(msg) + + # check max size + if mx and salt_size > mx: + msg = "%s: %s (%r) above max_salt_size (%d)" % (cls.name, param, salt_size, mx) + if relaxed: + warn(msg, PasslibHashWarning) + salt_size = mx + else: + raise ValueError(msg) + + return salt_size + + #=================================================================== + # init + #=================================================================== + def __init__(self, salt=None, **kwds): + super(HasSalt, self).__init__(**kwds) + if salt is not None: + salt = self._parse_salt(salt) + elif self.use_defaults: + salt = self._generate_salt() + assert self._norm_salt(salt) == salt, "generated invalid salt: %r" % (salt,) + else: + raise TypeError("no salt specified") + self.salt = salt + + # NOTE: split out mainly so sha256_crypt can subclass this + def _parse_salt(self, salt): + return self._norm_salt(salt) + + @classmethod + def _norm_salt(cls, salt, relaxed=False): + """helper to normalize & validate user-provided salt string + + :arg salt: + salt string + + :raises TypeError: + If salt not correct type. + + :raises ValueError: + + * if salt contains chars that aren't in :attr:`salt_chars`. + * if salt contains less than :attr:`min_salt_size` characters. + * if ``relaxed=False`` and salt has more than :attr:`max_salt_size` + characters (if ``relaxed=True``, the salt is truncated + and a warning is issued instead). + + :returns: + normalized salt + """ + # check type + if cls._salt_is_bytes: + if not isinstance(salt, bytes): + raise exc.ExpectedTypeError(salt, "bytes", "salt") + else: + if not isinstance(salt, unicode): + # NOTE: allowing bytes under py2 so salt can be native str. + if isinstance(salt, bytes) and (PY2 or relaxed): + salt = salt.decode("ascii") + else: + raise exc.ExpectedTypeError(salt, "unicode", "salt") + + # check charset + sc = cls.salt_chars + if sc is not None and any(c not in sc for c in salt): + raise ValueError("invalid characters in %s salt" % cls.name) + + # check min size + mn = cls.min_salt_size + if mn and len(salt) < mn: + msg = "salt too small (%s requires %s %d %s)" % (cls.name, + "exactly" if mn == cls.max_salt_size else ">=", mn, cls._salt_unit) + raise ValueError(msg) + + # check max size + mx = cls.max_salt_size + if mx and len(salt) > mx: + msg = "salt too large (%s requires %s %d %s)" % (cls.name, + "exactly" if mx == mn else "<=", mx, cls._salt_unit) + if relaxed: + warn(msg, PasslibHashWarning) + salt = cls._truncate_salt(salt, mx) + else: + raise ValueError(msg) + + return salt + + @staticmethod + def _truncate_salt(salt, mx): + # NOTE: some hashes (e.g. bcrypt) has structure within their + # salt string. this provides a method to override to perform + # the truncation properly + return salt[:mx] + + @classmethod + def _generate_salt(cls): + """ + helper method for _init_salt(); generates a new random salt string. + """ + return getrandstr(rng, cls.default_salt_chars, cls.default_salt_size) + + @classmethod + def bitsize(cls, salt_size=None, **kwds): + """[experimental method] return info about bitsizes of hash""" + info = super(HasSalt, cls).bitsize(**kwds) + if salt_size is None: + salt_size = cls.default_salt_size + # FIXME: this may overestimate size due to padding bits + # FIXME: this will be off by 1 for case-insensitive hashes. + info['salt'] = _bitsize(salt_size, cls.default_salt_chars) + return info + + #=================================================================== + # eoc + #=================================================================== + +class HasRawSalt(HasSalt): + """mixin for classes which use decoded salt parameter + + A variant of :class:`!HasSalt` which takes in decoded bytes instead of an encoded string. + + .. todo:: + + document this class's usage + """ + + salt_chars = ALL_BYTE_VALUES + + # NOTE: all HasRawSalt code is currently part of HasSalt, using private + # '_salt_is_bytes' flag. this arrangement may be changed in the future. + _salt_is_bytes = True + _salt_unit = "bytes" + + @classmethod + def _generate_salt(cls): + assert cls.salt_chars in [None, ALL_BYTE_VALUES] + return getrandbytes(rng, cls.default_salt_size) + +#------------------------------------------------------------------------ +# rounds mixin +#------------------------------------------------------------------------ +class HasRounds(GenericHandler): + """mixin for validating rounds parameter + + This :class:`GenericHandler` mixin adds a ``rounds`` keyword to the class + constuctor; any value provided is passed through the :meth:`_norm_rounds` + method, which takes care of validating the number of rounds. + + :param rounds: optional number of rounds hash should use + + Class Attributes + ================ + In order for :meth:`!_norm_rounds` to do its job, the following + attributes must be provided by the handler subclass: + + .. attribute:: min_rounds + + The minimum number of rounds allowed. A :exc:`ValueError` will be + thrown if the rounds value is too small. Defaults to ``0``. + + .. attribute:: max_rounds + + The maximum number of rounds allowed. A :exc:`ValueError` will be + thrown if the rounds value is larger than this. Defaults to ``None`` + which indicates no limit to the rounds value. + + .. attribute:: default_rounds + + If no rounds value is provided to constructor, this value will be used. + If this is not specified, a rounds value *must* be specified by the + application. + + .. attribute:: rounds_cost + + [required] + The ``rounds`` parameter typically encodes a cpu-time cost + for calculating a hash. This should be set to ``"linear"`` + (the default) or ``"log2"``, depending on how the rounds value relates + to the actual amount of time that will be required. + + Class Methods + ============= + .. todo:: document using() and needs_update() options + + Instance Attributes + =================== + .. attribute:: rounds + + This instance attribute will be filled in with the rounds value provided + to the constructor (as adapted by :meth:`_norm_rounds`) + + Subclassable Methods + ==================== + .. automethod:: _norm_rounds + """ + #=================================================================== + # class attrs + #=================================================================== + + #----------------- + # algorithm options -- not application configurable + #----------------- + # XXX: rename to min_valid_rounds / max_valid_rounds, + # to clarify role compared to min_desired_rounds / max_desired_rounds? + min_rounds = 0 + max_rounds = None + rounds_cost = "linear" # default to the common case + + # hack to pass info to _CryptRecord (will be removed in passlib 2.0) + using_rounds_kwds = ("min_desired_rounds", "max_desired_rounds", + "min_rounds", "max_rounds", + "default_rounds", "vary_rounds") + + #----------------- + # desired & default rounds -- configurable via .using() classmethod + #----------------- + min_desired_rounds = None + max_desired_rounds = None + default_rounds = None + vary_rounds = None + + #=================================================================== + # instance attrs + #=================================================================== + rounds = None + + #=================================================================== + # variant constructor + #=================================================================== + @classmethod + def using(cls, # keyword only... + min_desired_rounds=None, max_desired_rounds=None, + default_rounds=None, vary_rounds=None, + min_rounds=None, max_rounds=None, rounds=None, # aliases used by CryptContext + **kwds): + + # check for aliases used by CryptContext + if min_rounds is not None: + if min_desired_rounds is not None: + raise TypeError("'min_rounds' and 'min_desired_rounds' aliases are mutually exclusive") + min_desired_rounds = min_rounds + + if max_rounds is not None: + if max_desired_rounds is not None: + raise TypeError("'max_rounds' and 'max_desired_rounds' aliases are mutually exclusive") + max_desired_rounds = max_rounds + + # use 'rounds' as fallback for min, max, AND default + # XXX: would it be better to make 'default_rounds' and 'rounds' + # aliases, and have a separate 'require_rounds' parameter for this behavior? + if rounds is not None: + if min_desired_rounds is None: + min_desired_rounds = rounds + if max_desired_rounds is None: + max_desired_rounds = rounds + if default_rounds is None: + default_rounds = rounds + + # generate new subclass + subcls = super(HasRounds, cls).using(**kwds) + + # replace min_desired_rounds + relaxed = kwds.get("relaxed") + if min_desired_rounds is None: + explicit_min_rounds = False + min_desired_rounds = cls.min_desired_rounds + else: + explicit_min_rounds = True + if isinstance(min_desired_rounds, native_string_types): + min_desired_rounds = int(min_desired_rounds) + subcls.min_desired_rounds = subcls._norm_rounds(min_desired_rounds, + param="min_desired_rounds", + relaxed=relaxed) + + # replace max_desired_rounds + if max_desired_rounds is None: + max_desired_rounds = cls.max_desired_rounds + else: + if isinstance(max_desired_rounds, native_string_types): + max_desired_rounds = int(max_desired_rounds) + if min_desired_rounds and max_desired_rounds < min_desired_rounds: + msg = "%s: max_desired_rounds (%r) below min_desired_rounds (%r)" % \ + (subcls.name, max_desired_rounds, min_desired_rounds) + if explicit_min_rounds: + raise ValueError(msg) + else: + warn(msg, PasslibConfigWarning) + max_desired_rounds = min_desired_rounds + subcls.max_desired_rounds = subcls._norm_rounds(max_desired_rounds, + param="max_desired_rounds", + relaxed=relaxed) + + # replace default_rounds + if default_rounds is not None: + if isinstance(default_rounds, native_string_types): + default_rounds = int(default_rounds) + if min_desired_rounds and default_rounds < min_desired_rounds: + raise ValueError("%s: default_rounds (%r) below min_desired_rounds (%r)" % + (subcls.name, default_rounds, min_desired_rounds)) + elif max_desired_rounds and default_rounds > max_desired_rounds: + raise ValueError("%s: default_rounds (%r) above max_desired_rounds (%r)" % + (subcls.name, default_rounds, max_desired_rounds)) + subcls.default_rounds = subcls._norm_rounds(default_rounds, + param="default_rounds", + relaxed=relaxed) + + # clip default rounds to new limits. + if subcls.default_rounds is not None: + subcls.default_rounds = subcls._clip_to_desired_rounds(subcls.default_rounds) + + # replace / set vary_rounds + if vary_rounds is not None: + if isinstance(vary_rounds, native_string_types): + if vary_rounds.endswith("%"): + vary_rounds = float(vary_rounds[:-1]) * 0.01 + elif "." in vary_rounds: + vary_rounds = float(vary_rounds) + else: + vary_rounds = int(vary_rounds) + if vary_rounds < 0: + raise ValueError("%s: vary_rounds (%r) below 0" % + (subcls.name, vary_rounds)) + elif isinstance(vary_rounds, float): + # TODO: deprecate / disallow vary_rounds=1.0 + if vary_rounds > 1: + raise ValueError("%s: vary_rounds (%r) above 1.0" % + (subcls.name, vary_rounds)) + elif not isinstance(vary_rounds, int): + raise TypeError("vary_rounds must be int or float") + if vary_rounds: + warn("The 'vary_rounds' option is deprecated as of Passlib 1.7, " + "and will be removed in Passlib 2.0", PasslibConfigWarning) + subcls.vary_rounds = vary_rounds + # XXX: could cache _calc_vary_rounds_range() here if needed, + # but would need to handle user manually changing .default_rounds + return subcls + + @classmethod + def _clip_to_desired_rounds(cls, rounds): + """ + helper for :meth:`_generate_rounds` -- + clips rounds value to desired min/max set by class (if any) + """ + # NOTE: min/max_desired_rounds are None if unset. + # check minimum + mnd = cls.min_desired_rounds or 0 + if rounds < mnd: + return mnd + + # check maximum + mxd = cls.max_desired_rounds + if mxd and rounds > mxd: + return mxd + + return rounds + + @classmethod + def _calc_vary_rounds_range(cls, default_rounds): + """ + helper for :meth:`_generate_rounds` -- + returns range for vary rounds generation. + + :returns: + (lower, upper) limits suitable for random.randint() + """ + # XXX: could precalculate output of this in using() method, and save per-hash cost. + # but then users patching cls.vary_rounds / cls.default_rounds would get wrong value. + assert default_rounds + vary_rounds = cls.vary_rounds + + # if vary_rounds specified as % of default, convert it to actual rounds + def linear_to_native(value, upper): + return value + if isinstance(vary_rounds, float): + assert 0 <= vary_rounds <= 1 # TODO: deprecate vary_rounds==1 + if cls.rounds_cost == "log2": + # special case -- have to convert default_rounds to linear scale, + # apply +/- vary_rounds to that, and convert back to log scale again. + # linear_to_native() takes care of the "convert back" step. + default_rounds = 1 << default_rounds + def linear_to_native(value, upper): + if value <= 0: # log() undefined for <= 0 + return 0 + elif upper: # use smallest upper bound for start of range + return int(math.log(value, 2)) + else: # use greatest lower bound for end of range + return int(math.ceil(math.log(value, 2))) + # calculate integer vary rounds based on current default_rounds + vary_rounds = int(default_rounds * vary_rounds) + + # calculate bounds based on default_rounds +/- vary_rounds + assert vary_rounds >= 0 and isinstance(vary_rounds, int_types) + lower = linear_to_native(default_rounds - vary_rounds, False) + upper = linear_to_native(default_rounds + vary_rounds, True) + return cls._clip_to_desired_rounds(lower), cls._clip_to_desired_rounds(upper) + + #=================================================================== + # init + #=================================================================== + def __init__(self, rounds=None, **kwds): + super(HasRounds, self).__init__(**kwds) + if rounds is not None: + rounds = self._parse_rounds(rounds) + elif self.use_defaults: + rounds = self._generate_rounds() + assert self._norm_rounds(rounds) == rounds, "generated invalid rounds: %r" % (rounds,) + else: + raise TypeError("no rounds specified") + self.rounds = rounds + + # NOTE: split out mainly so sha256_crypt & bsdi_crypt can subclass this + def _parse_rounds(self, rounds): + return self._norm_rounds(rounds) + + @classmethod + def _norm_rounds(cls, rounds, relaxed=False, param="rounds"): + """ + helper for normalizing rounds value. + + :arg rounds: + an integer cost parameter. + + :param relaxed: + if ``True`` (the default), issues PasslibHashWarning is rounds are outside allowed range. + if ``False``, raises a ValueError instead. + + :param param: + optional name of parameter to insert into error/warning messages. + + :raises TypeError: + * if ``use_defaults=False`` and no rounds is specified + * if rounds is not an integer. + + :raises ValueError: + + * if rounds is ``None`` and class does not specify a value for + :attr:`default_rounds`. + * if ``relaxed=False`` and rounds is outside bounds of + :attr:`min_rounds` and :attr:`max_rounds` (if ``relaxed=True``, + the rounds value will be clamped, and a warning issued). + + :returns: + normalized rounds value + """ + return norm_integer(cls, rounds, cls.min_rounds, cls.max_rounds, + param=param, relaxed=relaxed) + + @classmethod + def _generate_rounds(cls): + """ + internal helper for :meth:`_norm_rounds` -- + returns default rounds value, incorporating vary_rounds, + and any other limitations hash may place on rounds parameter. + """ + # load default rounds + rounds = cls.default_rounds + if rounds is None: + raise TypeError("%s rounds value must be specified explicitly" % (cls.name,)) + + # randomly vary the rounds slightly basic on vary_rounds parameter. + # reads default_rounds internally. + if cls.vary_rounds: + lower, upper = cls._calc_vary_rounds_range(rounds) + assert lower <= rounds <= upper + if lower < upper: + rounds = rng.randint(lower, upper) + + return rounds + + #=================================================================== + # migration interface + #=================================================================== + def _calc_needs_update(self, **kwds): + """ + mark hash as needing update if rounds is outside desired bounds. + """ + min_desired_rounds = self.min_desired_rounds + if min_desired_rounds and self.rounds < min_desired_rounds: + return True + max_desired_rounds = self.max_desired_rounds + if max_desired_rounds and self.rounds > max_desired_rounds: + return True + return super(HasRounds, self)._calc_needs_update(**kwds) + + #=================================================================== + # experimental methods + #=================================================================== + @classmethod + def bitsize(cls, rounds=None, vary_rounds=.1, **kwds): + """[experimental method] return info about bitsizes of hash""" + info = super(HasRounds, cls).bitsize(**kwds) + # NOTE: this essentially estimates how many bits of "salt" + # can be added by varying the rounds value just a little bit. + if cls.rounds_cost != "log2": + # assume rounds can be randomized within the range + # rounds*(1-vary_rounds) ... rounds*(1+vary_rounds) + # then this can be used to encode + # log2(rounds*(1+vary_rounds)-rounds*(1-vary_rounds)) + # worth of salt-like bits. this works out to + # 1+log2(rounds*vary_rounds) + import math + if rounds is None: + rounds = cls.default_rounds + info['rounds'] = max(0, int(1+math.log(rounds*vary_rounds,2))) + ## else: # log2 rounds + # all bits of the rounds value are critical to choosing + # the time-cost, and can't be randomized. + return info + + #=================================================================== + # eoc + #=================================================================== + +#------------------------------------------------------------------------ +# other common parameters +#------------------------------------------------------------------------ +class ParallelismMixin(GenericHandler): + """ + mixin which provides common behavior for 'parallelism' setting + """ + #=================================================================== + # class attrs + #=================================================================== + + # NOTE: subclasses should add "parallelism" to their settings_kwds + + #=================================================================== + # instance attrs + #=================================================================== + + #: parallelism setting (class-level value used as default) + parallelism = 1 + + #=================================================================== + # variant constructor + #=================================================================== + + @classmethod + def using(cls, parallelism=None, **kwds): + subcls = super(ParallelismMixin, cls).using(**kwds) + if parallelism is not None: + if isinstance(parallelism, native_string_types): + parallelism = int(parallelism) + subcls.parallelism = subcls._norm_parallelism(parallelism, relaxed=kwds.get("relaxed")) + return subcls + + #=================================================================== + # init + #=================================================================== + def __init__(self, parallelism=None, **kwds): + super(ParallelismMixin, self).__init__(**kwds) + + # init parallelism + if parallelism is None: + assert validate_default_value(self, self.parallelism, self._norm_parallelism, + param="parallelism") + else: + self.parallelism = self._norm_parallelism(parallelism) + + @classmethod + def _norm_parallelism(cls, parallelism, relaxed=False): + return norm_integer(cls, parallelism, min=1, param="parallelism", relaxed=relaxed) + + #=================================================================== + # hash migration + #=================================================================== + + def _calc_needs_update(self, **kwds): + """ + mark hash as needing update if rounds is outside desired bounds. + """ + # XXX: for now, marking all hashes which don't have matching parallelism setting + if self.parallelism != type(self).parallelism: + return True + return super(ParallelismMixin, self)._calc_needs_update(**kwds) + + #=================================================================== + # eoc + #=================================================================== + +#------------------------------------------------------------------------ +# backend mixin & helpers +#------------------------------------------------------------------------ + +#: global lock that must be held when changing backends. +#: not bothering to make this more granular, as backend switching +#: isn't a speed-critical path. lock is needed since there is some +#: class-level state that may be modified during a "dry run" +_backend_lock = threading.RLock() + +class BackendMixin(PasswordHash): + """ + PasswordHash mixin which provides generic framework for supporting multiple backends + within the class. + + Public API + ---------- + + .. attribute:: backends + + This attribute should be a tuple containing the names of the backends + which are supported. Two common names are ``"os_crypt"`` (if backend + uses :mod:`crypt`), and ``"builtin"`` (if the backend is a pure-python + fallback). + + .. automethod:: get_backend + .. automethod:: set_backend + .. automethod:: has_backend + + .. warning:: + + :meth:`set_backend` is intended to be called during application startup -- + it affects global state, and switching backends is not guaranteed threadsafe. + + Private API (Subclass Hooks) + ---------------------------- + Subclasses should set the :attr:`!backends` attribute to a tuple of the backends + they wish to support. They should also define one method: + + .. classmethod:: _load_backend_{name}(dryrun=False) + + One copy of this method should be defined for each :samp:`name` within :attr:`!backends`. + + It will be called in order to load the backend, and should take care of whatever + is needed to enable the backend. This may include importing modules, running tests, + issuing warnings, etc. + + :param name: + [Optional] name of backend. + + :param dryrun: + [Optional] True/False if currently performing a "dry run". + + if True, the method should perform all setup actions *except* + switching the class over to the new backend. + + :raises passlib.exc.PasslibSecurityError: + if the backend is available, but cannot be loaded due to a security issue. + + :returns: + False if backend not available, True if backend loaded. + + .. warning:: + + Due to the way passlib's internals are arranged, + backends should generally store stateful data at the class level + (not the module level), and be prepared to be called on subclasses + which may be set to a different backend from their parent. + + (Idempotent module-level data such as lazy imports are fine). + + .. automethod:: _finalize_backend + + .. versionadded:: 1.7 + """ + #=================================================================== + # class attrs + #=================================================================== + + #: list of backend names, provided by subclass. + backends = None + + #: private attr mixin uses to hold currently loaded backend (or ``None``) + __backend = None + + #: optional class-specific text containing suggestion about what to do + #: when no backends are available. + _no_backend_suggestion = None + + #: shared attr used by set_backend() to indicate what backend it's loaded; + #: meaningless while not in set_backend(). + _pending_backend = None + + #: shared attr used by set_backend() to indicate if it's in "dry run" mode; + #: meaningless while not in set_backend(). + _pending_dry_run = False + + #=================================================================== + # public api + #=================================================================== + + @classmethod + def get_backend(cls): + """ + Return name of currently active backend. + if no backend has been loaded, loads and returns name of default backend. + + :raises passlib.exc.MissingBackendError: + if no backends are available. + + :returns: + name of active backend + """ + if not cls.__backend: + cls.set_backend() + assert cls.__backend, "set_backend() failed to load a default backend" + return cls.__backend + + @classmethod + def has_backend(cls, name="any"): + """ + Check if support is currently available for specified backend. + + :arg name: + name of backend to check for. + can be any string accepted by :meth:`set_backend`. + + :raises ValueError: + if backend name is unknown + + :returns: + * ``True`` if backend is available. + * ``False`` if it's available / can't be loaded. + * ``None`` if it's present, but won't load due to a security issue. + """ + try: + cls.set_backend(name, dryrun=True) + return True + except (exc.MissingBackendError, exc.PasslibSecurityError): + return False + + @classmethod + def set_backend(cls, name="any", dryrun=False): + """ + Load specified backend. + + :arg name: + name of backend to load, can be any of the following: + + * ``"any"`` -- use current backend if one is loaded, + otherwise load the first available backend. + + * ``"default"`` -- use the first available backend. + + * any string in :attr:`backends`, loads specified backend. + + :param dryrun: + If True, this perform all setup actions *except* switching over to the new backend. + (this flag is used to implement :meth:`has_backend`). + + .. versionadded:: 1.7 + + :raises ValueError: + If backend name is unknown. + + :raises passlib.exc.MissingBackendError: + If specific backend is missing; + or in the case of ``"any"`` / ``"default"``, if *no* backends are available. + + :raises passlib.exc.PasslibSecurityError: + + If ``"any"`` or ``"default"`` was specified, + but the only backend available has a PasslibSecurityError. + """ + # check if active backend is acceptable + if (name == "any" and cls.__backend) or (name and name == cls.__backend): + return cls.__backend + + # if this isn't the final subclass, whose bases we can modify, + # find that class, and recursively call this method for the proper class. + owner = cls._get_backend_owner() + if owner is not cls: + return owner.set_backend(name, dryrun=dryrun) + + # pick first available backend + if name == "any" or name == "default": + default_error = None + for name in cls.backends: + try: + return cls.set_backend(name, dryrun=dryrun) + except exc.MissingBackendError: + continue + except exc.PasslibSecurityError as err: + # backend is available, but refuses to load due to security issue. + if default_error is None: + default_error = err + continue + if default_error is None: + msg = "%s: no backends available" % cls.name + if cls._no_backend_suggestion: + msg += cls._no_backend_suggestion + default_error = exc.MissingBackendError(msg) + raise default_error + + # validate name + if name not in cls.backends: + raise exc.UnknownBackendError(cls, name) + + # hand off to _set_backend() + with _backend_lock: + orig = cls._pending_backend, cls._pending_dry_run + try: + cls._pending_backend = name + cls._pending_dry_run = dryrun + cls._set_backend(name, dryrun) + finally: + cls._pending_backend, cls._pending_dry_run = orig + if not dryrun: + cls.__backend = name + return name + + #=================================================================== + # subclass hooks + #=================================================================== + + @classmethod + def _get_backend_owner(cls): + """ + return class that set_backend() should actually be modifying. + for SubclassBackendMixin, this may not always be the class that was invoked. + """ + return cls + + @classmethod + def _set_backend(cls, name, dryrun): + """ + Internal method invoked by :meth:`set_backend`. + handles actual loading of specified backend. + + global _backend_lock will be held for duration of this method, + and _pending_dry_run & _pending_backend will also be set. + + should return True / False. + """ + loader = cls._get_backend_loader(name) + kwds = {} + if accepts_keyword(loader, "name"): + kwds['name'] = name + if accepts_keyword(loader, "dryrun"): + kwds['dryrun'] = dryrun + ok = loader(**kwds) + if ok is False: + raise exc.MissingBackendError("%s: backend not available: %s" % + (cls.name, name)) + elif ok is not True: + raise AssertionError("backend loaders must return True or False" + ": %r" % (ok,)) + + @classmethod + def _get_backend_loader(cls, name): + """ + Hook called to get the specified backend's loader. + Should return callable which optionally takes ``"name"`` and/or + ``"dryrun"`` keywords. + + Callable should return True if backend initialized successfully. + + If backend can't be loaded, callable should return False + OR raise MissingBackendError directly. + """ + raise NotImplementedError("implement in subclass") + + @classmethod + def _stub_requires_backend(cls): + """ + helper for subclasses to create stub methods which auto-load backend. + """ + if cls.__backend: + raise AssertionError("%s: _finalize_backend(%r) failed to replace lazy loader" % + (cls.name, cls.__backend)) + cls.set_backend() + if not cls.__backend: + raise AssertionError("%s: set_backend() failed to load a default backend" % + (cls.name)) + + #=================================================================== + # eoc + #=================================================================== + +class SubclassBackendMixin(BackendMixin): + """ + variant of BackendMixin which allows backends to be implemented + as separate mixin classes, and dynamically switches them out. + + backend classes should implement a _load_backend() classmethod, + which will be invoked with an optional 'dryrun' keyword, + and should return True or False. + + _load_backend() will be invoked with ``cls`` equal to the mixin, + *not* the overall class. + + .. versionadded:: 1.7 + """ + #=================================================================== + # class attrs + #=================================================================== + + # 'backends' required by BackendMixin + + #: NON-INHERITED flag that this class's bases should be modified by SubclassBackendMixin. + #: should only be set to True in *one* subclass in hierarchy. + _backend_mixin_target = False + + #: map of backend name -> mixin class + _backend_mixin_map = None + + #=================================================================== + # backend loading + #=================================================================== + + @classmethod + def _get_backend_owner(cls): + """ + return base class that we're actually switching backends on + (needed in since backends frequently modify class attrs, + and .set_backend may be called from a subclass). + """ + if not cls._backend_mixin_target: + raise AssertionError("_backend_mixin_target not set") + for base in cls.__mro__: + if base.__dict__.get("_backend_mixin_target"): + return base + raise AssertionError("expected to find class w/ '_backend_mixin_target' set") + + @classmethod + def _set_backend(cls, name, dryrun): + # invoke backend loader (will throw error if fails) + super(SubclassBackendMixin, cls)._set_backend(name, dryrun) + + # sanity check call args (should trust .set_backend, but will really + # foul things up if this isn't the owner) + assert cls is cls._get_backend_owner(), "_finalize_backend() not invoked on owner" + + # pick mixin class + mixin_map = cls._backend_mixin_map + assert mixin_map, "_backend_mixin_map not specified" + mixin_cls = mixin_map[name] + assert issubclass(mixin_cls, SubclassBackendMixin), "invalid mixin class" + + # modify to remove existing backend mixins, and insert the new one + update_mixin_classes(cls, + add=mixin_cls, + remove=mixin_map.values(), + append=True, before=SubclassBackendMixin, + dryrun=dryrun, + ) + + @classmethod + def _get_backend_loader(cls, name): + assert cls._backend_mixin_map, "_backend_mixin_map not specified" + return cls._backend_mixin_map[name]._load_backend_mixin + + #=================================================================== + # eoc + #=================================================================== + +# XXX: rename to ChecksumBackendMixin? +class HasManyBackends(BackendMixin, GenericHandler): + """ + GenericHandler mixin which provides selecting from multiple backends. + + .. todo:: + + finish documenting this class's usage + + For hashes which need to select from multiple backends, + depending on the host environment, this class + offers a way to specify alternate :meth:`_calc_checksum` methods, + and will dynamically chose the best one at runtime. + + .. versionchanged:: 1.7 + + This class now derives from :class:`BackendMixin`, which abstracts + out a more generic framework for supporting multiple backends. + The public api (:meth:`!get_backend`, :meth:`!has_backend`, :meth:`!set_backend`) + is roughly the same. + + Private API (Subclass Hooks) + ---------------------------- + As of version 1.7, classes should implement :meth:`!_load_backend_{name}`, per + :class:`BackendMixin`. This hook should invoke :meth:`!_set_calc_checksum_backcend` + to install it's backend method. + + .. deprecated:: 1.7 + + The following api is deprecated, and will be removed in Passlib 2.0: + + .. attribute:: _has_backend_{name} + + private class attribute checked by :meth:`has_backend` to see if a + specific backend is available, it should be either ``True`` + or ``False``. One of these should be provided by + the subclass for each backend listed in :attr:`backends`. + + .. classmethod:: _calc_checksum_{name} + + private class method that should implement :meth:`_calc_checksum` + for a given backend. it will only be called if the backend has + been selected by :meth:`set_backend`. One of these should be provided + by the subclass for each backend listed in :attr:`backends`. + """ + #=================================================================== + # digest calculation + #=================================================================== + + def _calc_checksum(self, secret): + "wrapper for backend, for common code""" + # NOTE: not overwriting _calc_checksum() directly, so that classes can provide + # common behavior in that method, + # and then invoke _calc_checksum_backend() to do the work. + return self._calc_checksum_backend(secret) + + def _calc_checksum_backend(self, secret): + """ + stub for _calc_checksum_backend() -- + should load backend if one hasn't been loaded; + if one has been loaded, this method should have been monkeypatched by _finalize_backend(). + """ + self._stub_requires_backend() + return self._calc_checksum_backend(secret) + + #=================================================================== + # BackendMixin hooks + #=================================================================== + @classmethod + def _get_backend_loader(cls, name): + """ + subclassed to support legacy 1.6 HasManyBackends api. + (will be removed in passlib 2.0) + """ + # check for 1.7 loader + loader = getattr(cls, "_load_backend_" + name, None) + if loader is None: + # fallback to pre-1.7 _has_backend_xxx + _calc_checksum_xxx() api + def loader(): + return cls.__load_legacy_backend(name) + else: + # make sure 1.6 api isn't defined at same time + assert not hasattr(cls, "_has_backend_" + name), ( + "%s: can't specify both ._load_backend_%s() " + "and ._has_backend_%s" % (cls.name, name, name) + ) + return loader + + @classmethod + def __load_legacy_backend(cls, name): + value = getattr(cls, "_has_backend_" + name) + warn("%s: support for ._has_backend_%s is deprecated as of Passlib 1.7, " + "and will be removed in Passlib 1.9/2.0, please implement " + "._load_backend_%s() instead" % (cls.name, name, name), + DeprecationWarning, + ) + if value: + func = getattr(cls, "_calc_checksum_" + name) + cls._set_calc_checksum_backend(func) + return True + else: + return False + + @classmethod + def _set_calc_checksum_backend(cls, func): + """ + helper used by subclasses to validate & set backend-specific + calc checksum helper. + """ + backend = cls._pending_backend + assert backend, "should only be called during set_backend()" + if not callable(func): + raise RuntimeError("%s: backend %r returned invalid callable: %r" % + (cls.name, backend, func)) + if not cls._pending_dry_run: + cls._calc_checksum_backend = func + + #=================================================================== + # eoc + #=================================================================== + +#============================================================================= +# wrappers +#============================================================================= +# XXX: should this inherit from PasswordHash? +class PrefixWrapper(object): + """wraps another handler, adding a constant prefix. + + instances of this class wrap another password hash handler, + altering the constant prefix that's prepended to the wrapped + handlers' hashes. + + this is used mainly by the :doc:`ldap crypt ` handlers; + such as :class:`~passlib.hash.ldap_md5_crypt` which wraps :class:`~passlib.hash.md5_crypt` and adds a ``{CRYPT}`` prefix. + + usage:: + + myhandler = PrefixWrapper("myhandler", "md5_crypt", prefix="$mh$", orig_prefix="$1$") + + :param name: name to assign to handler + :param wrapped: handler object or name of registered handler + :param prefix: identifying prefix to prepend to all hashes + :param orig_prefix: prefix to strip (defaults to ''). + :param lazy: if True and wrapped handler is specified by name, don't look it up until needed. + """ + + #: list of attributes which should be cloned by .using() + _using_clone_attrs = () + + def __init__(self, name, wrapped, prefix=u(''), orig_prefix=u(''), lazy=False, + doc=None, ident=None): + self.name = name + if isinstance(prefix, bytes): + prefix = prefix.decode("ascii") + self.prefix = prefix + if isinstance(orig_prefix, bytes): + orig_prefix = orig_prefix.decode("ascii") + self.orig_prefix = orig_prefix + if doc: + self.__doc__ = doc + if hasattr(wrapped, "name"): + self._set_wrapped(wrapped) + else: + self._wrapped_name = wrapped + if not lazy: + self._get_wrapped() + + if ident is not None: + if ident is True: + # signal that prefix is identifiable in itself. + if prefix: + ident = prefix + else: + raise ValueError("no prefix specified") + if isinstance(ident, bytes): + ident = ident.decode("ascii") + # XXX: what if ident includes parts of wrapped hash's ident? + if ident[:len(prefix)] != prefix[:len(ident)]: + raise ValueError("ident must agree with prefix") + self._ident = ident + + _wrapped_name = None + _wrapped_handler = None + + def _set_wrapped(self, handler): + # check this is a valid handler + if 'ident' in handler.setting_kwds and self.orig_prefix: + # TODO: look into way to fix the issues. + warn("PrefixWrapper: 'orig_prefix' option may not work correctly " + "for handlers which have multiple identifiers: %r" % + (handler.name,), exc.PasslibRuntimeWarning) + + # store reference + self._wrapped_handler = handler + + def _get_wrapped(self): + handler = self._wrapped_handler + if handler is None: + handler = get_crypt_handler(self._wrapped_name) + self._set_wrapped(handler) + return handler + + wrapped = property(_get_wrapped) + + _ident = False + + @property + def ident(self): + value = self._ident + if value is False: + value = None + # XXX: how will this interact with orig_prefix ? + # not exposing attrs for now if orig_prefix is set. + if not self.orig_prefix: + wrapped = self.wrapped + ident = getattr(wrapped, "ident", None) + if ident is not None: + value = self._wrap_hash(ident) + self._ident = value + return value + + _ident_values = False + + @property + def ident_values(self): + value = self._ident_values + if value is False: + value = None + # XXX: how will this interact with orig_prefix ? + # not exposing attrs for now if orig_prefix is set. + if not self.orig_prefix: + wrapped = self.wrapped + idents = getattr(wrapped, "ident_values", None) + if idents: + value = tuple(self._wrap_hash(ident) for ident in idents) + ##else: + ## ident = self.ident + ## if ident is not None: + ## value = [ident] + self._ident_values = value + return value + + # attrs that should be proxied + # XXX: change this to proxy everything that doesn't start with "_"? + _proxy_attrs = ( + "setting_kwds", "context_kwds", + "default_rounds", "min_rounds", "max_rounds", "rounds_cost", + "min_desired_rounds", "max_desired_rounds", "vary_rounds", + "default_salt_size", "min_salt_size", "max_salt_size", + "salt_chars", "default_salt_chars", + "backends", "has_backend", "get_backend", "set_backend", + "is_disabled", "truncate_size", "truncate_error", + "truncate_verify_reject", + + # internal info attrs needed for test inspection + "_salt_is_bytes", + ) + + def __repr__(self): + args = [ repr(self._wrapped_name or self._wrapped_handler) ] + if self.prefix: + args.append("prefix=%r" % self.prefix) + if self.orig_prefix: + args.append("orig_prefix=%r" % self.orig_prefix) + args = ", ".join(args) + return 'PrefixWrapper(%r, %s)' % (self.name, args) + + def __dir__(self): + attrs = set(dir(self.__class__)) + attrs.update(self.__dict__) + wrapped = self.wrapped + attrs.update( + attr for attr in self._proxy_attrs + if hasattr(wrapped, attr) + ) + return list(attrs) + + def __getattr__(self, attr): + """proxy most attributes from wrapped class (e.g. rounds, salt size, etc)""" + if attr in self._proxy_attrs: + return getattr(self.wrapped, attr) + raise AttributeError("missing attribute: %r" % (attr,)) + + def __setattr__(self, attr, value): + # if proxy attr present on wrapped object, + # and we own it, modify *it* instead. + # TODO: needs UTs + # TODO: any other cases where wrapped is "owned"? + # currently just if created via .using() + if attr in self._proxy_attrs and self._derived_from: + wrapped = self.wrapped + if hasattr(wrapped, attr): + setattr(wrapped, attr, value) + return + return object.__setattr__(self, attr, value) + + def _unwrap_hash(self, hash): + """given hash belonging to wrapper, return orig version""" + # NOTE: assumes hash has been validated as unicode already + prefix = self.prefix + if not hash.startswith(prefix): + raise exc.InvalidHashError(self) + # NOTE: always passing to handler as unicode, to save reconversion + return self.orig_prefix + hash[len(prefix):] + + def _wrap_hash(self, hash): + """given orig hash; return one belonging to wrapper""" + # NOTE: should usually be native string. + # (which does mean extra work under py2, but not py3) + if isinstance(hash, bytes): + hash = hash.decode("ascii") + orig_prefix = self.orig_prefix + if not hash.startswith(orig_prefix): + raise exc.InvalidHashError(self.wrapped) + wrapped = self.prefix + hash[len(orig_prefix):] + return uascii_to_str(wrapped) + + #: set by _using(), helper for test harness' handler_derived_from() + _derived_from = None + + def using(self, **kwds): + # generate subclass of wrapped handler + subcls = self.wrapped.using(**kwds) + assert subcls is not self.wrapped + # then create identical wrapper which wraps the new subclass. + wrapper = PrefixWrapper(self.name, subcls, prefix=self.prefix, orig_prefix=self.orig_prefix) + wrapper._derived_from = self + for attr in self._using_clone_attrs: + setattr(wrapper, attr, getattr(self, attr)) + return wrapper + + def needs_update(self, hash, **kwds): + hash = self._unwrap_hash(hash) + return self.wrapped.needs_update(hash, **kwds) + + def identify(self, hash): + hash = to_unicode_for_identify(hash) + if not hash.startswith(self.prefix): + return False + hash = self._unwrap_hash(hash) + return self.wrapped.identify(hash) + + @deprecated_method(deprecated="1.7", removed="2.0") + def genconfig(self, **kwds): + config = self.wrapped.genconfig(**kwds) + if config is None: + raise RuntimeError(".genconfig() must return a string, not None") + return self._wrap_hash(config) + + @deprecated_method(deprecated="1.7", removed="2.0") + def genhash(self, secret, config, **kwds): + # TODO: under 2.0, throw TypeError if config is None, rather than passing it through + if config is not None: + config = to_unicode(config, "ascii", "config/hash") + config = self._unwrap_hash(config) + return self._wrap_hash(self.wrapped.genhash(secret, config, **kwds)) + + @deprecated_method(deprecated="1.7", removed="2.0", replacement=".hash()") + def encrypt(self, secret, **kwds): + return self.hash(secret, **kwds) + + def hash(self, secret, **kwds): + return self._wrap_hash(self.wrapped.hash(secret, **kwds)) + + def verify(self, secret, hash, **kwds): + hash = to_unicode(hash, "ascii", "hash") + hash = self._unwrap_hash(hash) + return self.wrapped.verify(secret, hash, **kwds) + +#============================================================================= +# eof +#=============================================================================