codekingpro/portable-devtools
114k
1"""passlib.utils -- helpers for writing password hashes"""2#=============================================================================3# imports4#=============================================================================5from passlib.utils.compat import JYTHON6# core7from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError8from base64 import b64encode, b64decode9try:10 from collections.abc import Sequence11 from collections.abc import Iterable12except ImportError:13 # py2 compat14 from collections import Sequence15 from collections import Iterable16from codecs import lookup as _lookup_codec17from functools import update_wrapper18import itertools19import inspect20import logging; log = logging.getLogger(__name__)21import math22import os23import sys24import random25import re26if JYTHON: # pragma: no cover -- runtime detection27 # Jython 2.5.2 lacks stringprep module -28 # see http://bugs.jython.org/issue175832029 try:30 import stringprep31 except ImportError:32 stringprep = None33 _stringprep_missing_reason = "not present under Jython"34else:35 import stringprep36import time37if stringprep:38 import unicodedata39try:40 import threading41except ImportError:42 # module optional before py3743 threading = None44import timeit45import types46from warnings import warn47# site48# pkg49from passlib.utils.binary import (50 # [remove these aliases in 2.0]51 BASE64_CHARS, AB64_CHARS, HASH64_CHARS, BCRYPT_CHARS,52 Base64Engine, LazyBase64Engine, h64, h64big, bcrypt64,53 ab64_encode, ab64_decode, b64s_encode, b64s_decode54)55from passlib.utils.decor import (56 # [remove these aliases in 2.0]57 deprecated_function,58 deprecated_method,59 memoized_property,60 classproperty,61 hybrid_method,62)63from passlib.exc import ExpectedStringError, ExpectedTypeError64from passlib.utils.compat import (add_doc, join_bytes, join_byte_values,65 join_byte_elems, irange, imap, PY3, u,66 join_unicode, unicode, byte_elem_value, nextgetter,67 unicode_or_str, unicode_or_bytes_types,68 get_method_function, suppress_cause, PYPY)69# local70__all__ = [71 # constants72 'JYTHON',73 'sys_bits',74 'unix_crypt_schemes',75 'rounds_cost_values',76 77 # unicode helpers78 'consteq',79 'saslprep',80 81 # bytes helpers82 "xor_bytes",83 "render_bytes",84 85 # encoding helpers86 'is_same_codec',87 'is_ascii_safe',88 'to_bytes',89 'to_unicode',90 'to_native_str',91 92 # host OS93 'has_crypt',94 'test_crypt',95 'safe_crypt',96 'tick',97 98 # randomness99 'rng',100 'getrandbytes',101 'getrandstr',102 'generate_password',103 104 # object type / interface tests105 'is_crypt_handler',106 'is_crypt_context',107 'has_rounds_info',108 'has_salt_info',109]110 111#=============================================================================112# constants113#=============================================================================114 115# bitsize of system architecture (32 or 64)116sys_bits = int(math.log(sys.maxsize if PY3 else sys.maxint, 2) + 1.5)117 118# list of hashes algs supported by crypt() on at least one OS.119# XXX: move to .registry for passlib 2.0?120unix_crypt_schemes = [121 "sha512_crypt", "sha256_crypt",122 "sha1_crypt", "bcrypt",123 "md5_crypt",124 # "bsd_nthash",125 "bsdi_crypt", "des_crypt",126 ]127 128# list of rounds_cost constants129rounds_cost_values = [ "linear", "log2" ]130 131# legacy import, will be removed in 1.8132from passlib.exc import MissingBackendError133 134# internal helpers135_BEMPTY = b''136_UEMPTY = u("")137_USPACE = u(" ")138 139# maximum password size which passlib will allow; see exc.PasswordSizeError140MAX_PASSWORD_SIZE = int(os.environ.get("PASSLIB_MAX_PASSWORD_SIZE") or 4096)141 142#=============================================================================143# type helpers144#=============================================================================145 146class SequenceMixin(object):147 """148 helper which lets result object act like a fixed-length sequence.149 subclass just needs to provide :meth:`_as_tuple()`.150 """151 def _as_tuple(self):152 raise NotImplementedError("implement in subclass")153 154 def __repr__(self):155 return repr(self._as_tuple())156 157 def __getitem__(self, idx):158 return self._as_tuple()[idx]159 160 def __iter__(self):161 return iter(self._as_tuple())162 163 def __len__(self):164 return len(self._as_tuple())165 166 def __eq__(self, other):167 return self._as_tuple() == other168 169 def __ne__(self, other):170 return not self.__eq__(other)171 172if PY3:173 # getargspec() is deprecated, use this under py3.174 # even though it's a lot more awkward to get basic info :|175 176 _VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD177 _VAR_ANY_SET = set([_VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL])178 179 def accepts_keyword(func, key):180 """test if function accepts specified keyword"""181 params = inspect.signature(get_method_function(func)).parameters182 if not params:183 return False184 arg = params.get(key)185 if arg and arg.kind not in _VAR_ANY_SET:186 return True187 # XXX: annoying what we have to do to determine if VAR_KWDS in use.188 return params[list(params)[-1]].kind == _VAR_KEYWORD189 190else:191 192 def accepts_keyword(func, key):193 """test if function accepts specified keyword"""194 spec = inspect.getargspec(get_method_function(func))195 return key in spec.args or spec.keywords is not None196 197def update_mixin_classes(target, add=None, remove=None, append=False,198 before=None, after=None, dryrun=False):199 """200 helper to update mixin classes installed in target class.201 202 :param target:203 target class whose bases will be modified.204 205 :param add:206 class / classes to install into target's base class list.207 208 :param remove:209 class / classes to remove from target's base class list.210 211 :param append:212 by default, prepends mixins to front of list.213 if True, appends to end of list instead.214 215 :param after:216 optionally make sure all mixins are inserted after217 this class / classes.218 219 :param before:220 optionally make sure all mixins are inserted before221 this class / classes.222 223 :param dryrun:224 optionally perform all calculations / raise errors,225 but don't actually modify the class.226 """227 if isinstance(add, type):228 add = [add]229 230 bases = list(target.__bases__)231 232 # strip out requested mixins233 if remove:234 if isinstance(remove, type):235 remove = [remove]236 for mixin in remove:237 if add and mixin in add:238 continue239 if mixin in bases:240 bases.remove(mixin)241 242 # add requested mixins243 if add:244 for mixin in add:245 # if mixin already present (explicitly or not), leave alone246 if any(issubclass(base, mixin) for base in bases):247 continue248 249 # determine insertion point250 if append:251 for idx, base in enumerate(bases):252 if issubclass(mixin, base):253 # don't insert mixin after one of it's own bases254 break255 if before and issubclass(base, before):256 # don't insert mixin after any <before> classes.257 break258 else:259 # append to end260 idx = len(bases)261 elif after:262 for end_idx, base in enumerate(reversed(bases)):263 if issubclass(base, after):264 # don't insert mixin before any <after> classes.265 idx = len(bases) - end_idx266 assert bases[idx-1] == base267 break268 else:269 idx = 0270 else:271 # insert at start272 idx = 0273 274 # insert mixin275 bases.insert(idx, mixin)276 277 # modify class278 if not dryrun:279 target.__bases__ = tuple(bases)280 281#=============================================================================282# collection helpers283#=============================================================================284def batch(source, size):285 """286 split iterable into chunks of <size> elements.287 """288 if size < 1:289 raise ValueError("size must be positive integer")290 if isinstance(source, Sequence):291 end = len(source)292 i = 0293 while i < end:294 n = i + size295 yield source[i:n]296 i = n297 elif isinstance(source, Iterable):298 itr = iter(source)299 while True:300 chunk_itr = itertools.islice(itr, size)301 try:302 first = next(chunk_itr)303 except StopIteration:304 break305 yield itertools.chain((first,), chunk_itr)306 else:307 raise TypeError("source must be iterable")308 309#=============================================================================310# unicode helpers311#=============================================================================312 313# XXX: should this be moved to passlib.crypto, or compat backports?314 315def consteq(left, right):316 """Check two strings/bytes for equality.317 318 This function uses an approach designed to prevent319 timing analysis, making it appropriate for cryptography.320 a and b must both be of the same type: either str (ASCII only),321 or any type that supports the buffer protocol (e.g. bytes).322 323 Note: If a and b are of different lengths, or if an error occurs,324 a timing attack could theoretically reveal information about the325 types and lengths of a and b--but not their values.326 """327 # NOTE:328 # resources & discussions considered in the design of this function:329 # hmac timing attack --330 # http://rdist.root.org/2009/05/28/timing-attack-in-google-keyczar-library/331 # python developer discussion surrounding similar function --332 # http://bugs.python.org/issue15061333 # http://bugs.python.org/issue14955334 335 # validate types336 if isinstance(left, unicode):337 if not isinstance(right, unicode):338 raise TypeError("inputs must be both unicode or both bytes")339 is_py3_bytes = False340 elif isinstance(left, bytes):341 if not isinstance(right, bytes):342 raise TypeError("inputs must be both unicode or both bytes")343 is_py3_bytes = PY3344 else:345 raise TypeError("inputs must be both unicode or both bytes")346 347 # do size comparison.348 # NOTE: the double-if construction below is done deliberately, to ensure349 # the same number of operations (including branches) is performed regardless350 # of whether left & right are the same size.351 same_size = (len(left) == len(right))352 if same_size:353 # if sizes are the same, setup loop to perform actual check of contents.354 tmp = left355 result = 0356 if not same_size:357 # if sizes aren't the same, set 'result' so equality will fail regardless358 # of contents. then, to ensure we do exactly 'len(right)' iterations359 # of the loop, just compare 'right' against itself.360 tmp = right361 result = 1362 363 # run constant-time string comparision364 # TODO: use izip instead (but first verify it's faster than zip for this case)365 if is_py3_bytes:366 for l,r in zip(tmp, right):367 result |= l ^ r368 else:369 for l,r in zip(tmp, right):370 result |= ord(l) ^ ord(r)371 return result == 0372 373# keep copy of this around since stdlib's version throws error on non-ascii chars in unicode strings.374# our version does, but suffers from some underlying VM issues. but something is better than375# nothing for plaintext hashes, which need this. everything else should use consteq(),376# since the stdlib one is going to be as good / better in the general case.377str_consteq = consteq378 379try:380 # for py3.3 and up, use the stdlib version381 from hmac import compare_digest as consteq382except ImportError:383 pass384 385 # TODO: could check for cryptography package's version,386 # but only operates on bytes, so would need a wrapper,387 # or separate consteq() into a unicode & a bytes variant.388 # from cryptography.hazmat.primitives.constant_time import bytes_eq as consteq389 390def splitcomma(source, sep=","):391 """split comma-separated string into list of elements,392 stripping whitespace.393 """394 source = source.strip()395 if source.endswith(sep):396 source = source[:-1]397 if not source:398 return []399 return [ elem.strip() for elem in source.split(sep) ]400 401def saslprep(source, param="value"):402 """Normalizes unicode strings using SASLPrep stringprep profile.403 404 The SASLPrep profile is defined in :rfc:`4013`.405 It provides a uniform scheme for normalizing unicode usernames406 and passwords before performing byte-value sensitive operations407 such as hashing. Among other things, it normalizes diacritic408 representations, removes non-printing characters, and forbids409 invalid characters such as ``\\n``. Properly internationalized410 applications should run user passwords through this function411 before hashing.412 413 :arg source:414 unicode string to normalize & validate415 416 :param param:417 Optional noun identifying source parameter in error messages418 (Defaults to the string ``"value"``). This is mainly useful to make the caller's error419 messages make more sense contextually.420 421 :raises ValueError:422 if any characters forbidden by the SASLPrep profile are encountered.423 424 :raises TypeError:425 if input is not :class:`!unicode`426 427 :returns:428 normalized unicode string429 430 .. note::431 432 This function is not available under Jython,433 as the Jython stdlib is missing the :mod:`!stringprep` module434 (`Jython issue 1758320 <http://bugs.jython.org/issue1758320>`_).435 436 .. versionadded:: 1.6437 """438 # saslprep - http://tools.ietf.org/html/rfc4013439 # stringprep - http://tools.ietf.org/html/rfc3454440 # http://docs.python.org/library/stringprep.html441 442 # validate type443 # XXX: support bytes (e.g. run through want_unicode)?444 # might be easier to just integrate this into cryptcontext.445 if not isinstance(source, unicode):446 raise TypeError("input must be unicode string, not %s" %447 (type(source),))448 449 # mapping stage450 # - map non-ascii spaces to U+0020 (stringprep C.1.2)451 # - strip 'commonly mapped to nothing' chars (stringprep B.1)452 in_table_c12 = stringprep.in_table_c12453 in_table_b1 = stringprep.in_table_b1454 data = join_unicode(455 _USPACE if in_table_c12(c) else c456 for c in source457 if not in_table_b1(c)458 )459 460 # normalize to KC form461 data = unicodedata.normalize('NFKC', data)462 if not data:463 return _UEMPTY464 465 # check for invalid bi-directional strings.466 # stringprep requires the following:467 # - chars in C.8 must be prohibited.468 # - if any R/AL chars in string:469 # - no L chars allowed in string470 # - first and last must be R/AL chars471 # this checks if start/end are R/AL chars. if so, prohibited loop472 # will forbid all L chars. if not, prohibited loop will forbid all473 # R/AL chars instead. in both cases, prohibited loop takes care of C.8.474 is_ral_char = stringprep.in_table_d1475 if is_ral_char(data[0]):476 if not is_ral_char(data[-1]):477 raise ValueError("malformed bidi sequence in " + param)478 # forbid L chars within R/AL sequence.479 is_forbidden_bidi_char = stringprep.in_table_d2480 else:481 # forbid R/AL chars if start not setup correctly; L chars allowed.482 is_forbidden_bidi_char = is_ral_char483 484 # check for prohibited output - stringprep tables A.1, B.1, C.1.2, C.2 - C.9485 in_table_a1 = stringprep.in_table_a1486 in_table_c21_c22 = stringprep.in_table_c21_c22487 in_table_c3 = stringprep.in_table_c3488 in_table_c4 = stringprep.in_table_c4489 in_table_c5 = stringprep.in_table_c5490 in_table_c6 = stringprep.in_table_c6491 in_table_c7 = stringprep.in_table_c7492 in_table_c8 = stringprep.in_table_c8493 in_table_c9 = stringprep.in_table_c9494 for c in data:495 # check for chars mapping stage should have removed496 assert not in_table_b1(c), "failed to strip B.1 in mapping stage"497 assert not in_table_c12(c), "failed to replace C.1.2 in mapping stage"498 499 # check for forbidden chars500 if in_table_a1(c):501 raise ValueError("unassigned code points forbidden in " + param)502 if in_table_c21_c22(c):503 raise ValueError("control characters forbidden in " + param)504 if in_table_c3(c):505 raise ValueError("private use characters forbidden in " + param)506 if in_table_c4(c):507 raise ValueError("non-char code points forbidden in " + param)508 if in_table_c5(c):509 raise ValueError("surrogate codes forbidden in " + param)510 if in_table_c6(c):511 raise ValueError("non-plaintext chars forbidden in " + param)512 if in_table_c7(c):513 # XXX: should these have been caught by normalize?514 # if so, should change this to an assert515 raise ValueError("non-canonical chars forbidden in " + param)516 if in_table_c8(c):517 raise ValueError("display-modifying / deprecated chars "518 "forbidden in" + param)519 if in_table_c9(c):520 raise ValueError("tagged characters forbidden in " + param)521 522 # do bidi constraint check chosen by bidi init, above523 if is_forbidden_bidi_char(c):524 raise ValueError("forbidden bidi character in " + param)525 526 return data527 528# replace saslprep() with stub when stringprep is missing529if stringprep is None: # pragma: no cover -- runtime detection530 def saslprep(source, param="value"):531 """stub for saslprep()"""532 raise NotImplementedError("saslprep() support requires the 'stringprep' "533 "module, which is " + _stringprep_missing_reason)534 535#=============================================================================536# bytes helpers537#=============================================================================538def render_bytes(source, *args):539 """Peform ``%`` formating using bytes in a uniform manner across Python 2/3.540 541 This function is motivated by the fact that542 :class:`bytes` instances do not support ``%`` or ``{}`` formatting under Python 3.543 This function is an attempt to provide a replacement:544 it converts everything to unicode (decoding bytes instances as ``latin-1``),545 performs the required formatting, then encodes the result to ``latin-1``.546 547 Calling ``render_bytes(source, *args)`` should function roughly the same as548 ``source % args`` under Python 2.549 550 .. todo::551 python >= 3.5 added back limited support for bytes %,552 can revisit when 3.3/3.4 is dropped.553 """554 if isinstance(source, bytes):555 source = source.decode("latin-1")556 result = source % tuple(arg.decode("latin-1") if isinstance(arg, bytes)557 else arg for arg in args)558 return result.encode("latin-1")559 560if PY3:561 # new in py32562 def bytes_to_int(value):563 return int.from_bytes(value, 'big')564 def int_to_bytes(value, count):565 return value.to_bytes(count, 'big')566else:567 # XXX: can any of these be sped up?568 from binascii import hexlify, unhexlify569 def bytes_to_int(value):570 return int(hexlify(value),16)571 def int_to_bytes(value, count):572 return unhexlify(('%%0%dx' % (count<<1)) % value)573 574add_doc(bytes_to_int, "decode byte string as single big-endian integer")575add_doc(int_to_bytes, "encode integer as single big-endian byte string")576 577def xor_bytes(left, right):578 """Perform bitwise-xor of two byte strings (must be same size)"""579 return int_to_bytes(bytes_to_int(left) ^ bytes_to_int(right), len(left))580 581def repeat_string(source, size):582 """583 repeat or truncate <source> string, so it has length <size>584 """585 mult = 1 + (size - 1) // len(source)586 return (source * mult)[:size]587 588 589def utf8_repeat_string(source, size):590 """591 variant of repeat_string() which truncates to nearest UTF8 boundary.592 """593 mult = 1 + (size - 1) // len(source)594 return utf8_truncate(source * mult, size)595 596 597_BNULL = b"\x00"598_UNULL = u("\x00")599 600def right_pad_string(source, size, pad=None):601 """right-pad or truncate <source> string, so it has length <size>"""602 cur = len(source)603 if size > cur:604 if pad is None:605 pad = _UNULL if isinstance(source, unicode) else _BNULL606 return source+pad*(size-cur)607 else:608 return source[:size]609 610 611def utf8_truncate(source, index):612 """613 helper to truncate UTF8 byte string to nearest character boundary ON OR AFTER <index>.614 returned prefix will always have length of at least <index>, and will stop on the615 first byte that's not a UTF8 continuation byte (128 - 191 inclusive).616 since utf8 should never take more than 4 bytes to encode known unicode values,617 we can stop after ``index+3`` is reached.618 619 :param bytes source:620 :param int index:621 :rtype: bytes622 """623 # general approach:624 #625 # * UTF8 bytes will have high two bits (0xC0) as one of:626 # 00 -- ascii char627 # 01 -- ascii char628 # 10 -- continuation of multibyte char629 # 11 -- start of multibyte char.630 # thus we can cut on anything where high bits aren't "10" (0x80; continuation byte)631 #632 # * UTF8 characters SHOULD always be 1 to 4 bytes, though they may be unbounded.633 # so we just keep going until first non-continuation byte is encountered, or end of str.634 # this should work predictably even for malformed/non UTF8 inputs.635 636 if not isinstance(source, bytes):637 raise ExpectedTypeError(source, bytes, "source")638 639 # validate index640 end = len(source)641 if index < 0:642 index = max(0, index + end)643 if index >= end:644 return source645 646 # can stop search after 4 bytes, won't ever have longer utf8 sequence.647 end = min(index + 3, end)648 649 # loop until we find non-continuation byte650 while index < end:651 if byte_elem_value(source[index]) & 0xC0 != 0x80:652 # found single-char byte, or start-char byte.653 break654 # else: found continuation byte.655 index += 1656 else:657 assert index == end658 659 # truncate at final index660 result = source[:index]661 662 def sanity_check():663 # try to decode source664 try:665 text = source.decode("utf-8")666 except UnicodeDecodeError:667 # if source isn't valid utf8, byte level match is enough668 return True669 670 # validate that result was cut on character boundary671 assert text.startswith(result.decode("utf-8"))672 return True673 674 assert sanity_check()675 676 return result677 678#=============================================================================679# encoding helpers680#=============================================================================681_ASCII_TEST_BYTES = b"\x00\n aA:#!\x7f"682_ASCII_TEST_UNICODE = _ASCII_TEST_BYTES.decode("ascii")683 684def is_ascii_codec(codec):685 """Test if codec is compatible with 7-bit ascii (e.g. latin-1, utf-8; but not utf-16)"""686 return _ASCII_TEST_UNICODE.encode(codec) == _ASCII_TEST_BYTES687 688def is_same_codec(left, right):689 """Check if two codec names are aliases for same codec"""690 if left == right:691 return True692 if not (left and right):693 return False694 return _lookup_codec(left).name == _lookup_codec(right).name695 696_B80 = b'\x80'[0]697_U80 = u('\x80')698def is_ascii_safe(source):699 """Check if string (bytes or unicode) contains only 7-bit ascii"""700 r = _B80 if isinstance(source, bytes) else _U80701 return all(c < r for c in source)702 703def to_bytes(source, encoding="utf-8", param="value", source_encoding=None):704 """Helper to normalize input to bytes.705 706 :arg source:707 Source bytes/unicode to process.708 709 :arg encoding:710 Target encoding (defaults to ``"utf-8"``).711 712 :param param:713 Optional name of variable/noun to reference when raising errors714 715 :param source_encoding:716 If this is specified, and the source is bytes,717 the source will be transcoded from *source_encoding* to *encoding*718 (via unicode).719 720 :raises TypeError: if source is not unicode or bytes.721 722 :returns:723 * unicode strings will be encoded using *encoding*, and returned.724 * if *source_encoding* is not specified, byte strings will be725 returned unchanged.726 * if *source_encoding* is specified, byte strings will be transcoded727 to *encoding*.728 """729 assert encoding730 if isinstance(source, bytes):731 if source_encoding and not is_same_codec(source_encoding, encoding):732 return source.decode(source_encoding).encode(encoding)733 else:734 return source735 elif isinstance(source, unicode):736 return source.encode(encoding)737 else:738 raise ExpectedStringError(source, param)739 740def to_unicode(source, encoding="utf-8", param="value"):741 """Helper to normalize input to unicode.742 743 :arg source:744 source bytes/unicode to process.745 746 :arg encoding:747 encoding to use when decoding bytes instances.748 749 :param param:750 optional name of variable/noun to reference when raising errors.751 752 :raises TypeError: if source is not unicode or bytes.753 754 :returns:755 * returns unicode strings unchanged.756 * returns bytes strings decoded using *encoding*757 """758 assert encoding759 if isinstance(source, unicode):760 return source761 elif isinstance(source, bytes):762 return source.decode(encoding)763 else:764 raise ExpectedStringError(source, param)765 766if PY3:767 def to_native_str(source, encoding="utf-8", param="value"):768 if isinstance(source, bytes):769 return source.decode(encoding)770 elif isinstance(source, unicode):771 return source772 else:773 raise ExpectedStringError(source, param)774else:775 def to_native_str(source, encoding="utf-8", param="value"):776 if isinstance(source, bytes):777 return source778 elif isinstance(source, unicode):779 return source.encode(encoding)780 else:781 raise ExpectedStringError(source, param)782 783add_doc(to_native_str,784 """Take in unicode or bytes, return native string.785 786 Python 2: encodes unicode using specified encoding, leaves bytes alone.787 Python 3: leaves unicode alone, decodes bytes using specified encoding.788 789 :raises TypeError: if source is not unicode or bytes.790 791 :arg source:792 source unicode or bytes string.793 794 :arg encoding:795 encoding to use when encoding unicode or decoding bytes.796 this defaults to ``"utf-8"``.797 798 :param param:799 optional name of variable/noun to reference when raising errors.800 801 :returns: :class:`str` instance802 """)803 804@deprecated_function(deprecated="1.6", removed="1.7")805def to_hash_str(source, encoding="ascii"): # pragma: no cover -- deprecated & unused806 """deprecated, use to_native_str() instead"""807 return to_native_str(source, encoding, param="hash")808 809_true_set = set("true t yes y on 1 enable enabled".split())810_false_set = set("false f no n off 0 disable disabled".split())811_none_set = set(["", "none"])812 813def as_bool(value, none=None, param="boolean"):814 """815 helper to convert value to boolean.816 recognizes strings such as "true", "false"817 """818 assert none in [True, False, None]819 if isinstance(value, unicode_or_bytes_types):820 clean = value.lower().strip()821 if clean in _true_set:822 return True823 if clean in _false_set:824 return False825 if clean in _none_set:826 return none827 raise ValueError("unrecognized %s value: %r" % (param, value))828 elif isinstance(value, bool):829 return value830 elif value is None:831 return none832 else:833 return bool(value)834 835#=============================================================================836# host OS helpers837#=============================================================================838 839def is_safe_crypt_input(value):840 """841 UT helper --842 test if value is safe to pass to crypt.crypt();843 under PY3, can't pass non-UTF8 bytes to crypt.crypt.844 """845 if crypt_accepts_bytes or not isinstance(value, bytes):846 return True847 try:848 value.decode("utf-8")849 return True850 except UnicodeDecodeError:851 return False852 853try:854 from crypt import crypt as _crypt855except ImportError: # pragma: no cover856 _crypt = None857 has_crypt = False858 crypt_accepts_bytes = False859 crypt_needs_lock = False860 _safe_crypt_lock = None861 def safe_crypt(secret, hash):862 return None863else:864 has_crypt = True865 _NULL = '\x00'866 867 # XXX: replace this with lazy-evaluated bug detection?868 if threading and PYPY and (7, 2, 0) <= sys.pypy_version_info <= (7, 3, 3):869 #: internal lock used to wrap crypt() calls.870 #: WARNING: if non-passlib code invokes crypt(), this lock won't be enough!871 _safe_crypt_lock = threading.Lock()872 873 #: detect if crypt.crypt() needs a thread lock around calls.874 crypt_needs_lock = True875 876 else:877 from passlib.utils.compat import nullcontext878 _safe_crypt_lock = nullcontext()879 crypt_needs_lock = False880 881 # some crypt() variants will return various constant strings when882 # an invalid/unrecognized config string is passed in; instead of883 # returning NULL / None. examples include ":", ":0", "*0", etc.884 # safe_crypt() returns None for any string starting with one of the885 # chars in this string...886 _invalid_prefixes = u("*:!")887 888 if PY3:889 890 # * pypy3 (as of v7.3.1) has a crypt which accepts bytes, or ASCII-only unicode.891 # * whereas CPython3 (as of v3.9) has a crypt which doesn't take bytes,892 # but accepts ANY unicode (which it always encodes to UTF8).893 crypt_accepts_bytes = True894 try:895 _crypt(b"\xEE", "xx")896 except TypeError:897 # CPython will throw TypeError898 crypt_accepts_bytes = False899 except: # no pragma900 # don't care about other errors this might throw,901 # just want to see if we get past initial type-coercion step.902 pass903 904 def safe_crypt(secret, hash):905 if crypt_accepts_bytes:906 # PyPy3 -- all bytes accepted, but unicode encoded to ASCII,907 # so handling that ourselves.908 if isinstance(secret, unicode):909 secret = secret.encode("utf-8")910 if _BNULL in secret:911 raise ValueError("null character in secret")912 if isinstance(hash, unicode):913 hash = hash.encode("ascii")914 else:915 # CPython3's crypt() doesn't take bytes, only unicode; unicode which is then916 # encoding using utf-8 before passing to the C-level crypt().917 # so we have to decode the secret.918 if isinstance(secret, bytes):919 orig = secret920 try:921 secret = secret.decode("utf-8")922 except UnicodeDecodeError:923 return None924 # sanity check it encodes back to original byte string,925 # otherwise when crypt() does it's encoding, it'll hash the wrong bytes!926 assert secret.encode("utf-8") == orig, \927 "utf-8 spec says this can't happen!"928 if _NULL in secret:929 raise ValueError("null character in secret")930 if isinstance(hash, bytes):931 hash = hash.decode("ascii")932 try:933 with _safe_crypt_lock:934 result = _crypt(secret, hash)935 except OSError:936 # new in py39 -- per https://bugs.python.org/issue39289,937 # crypt() now throws OSError for various things, mainly unknown hash formats938 # translating that to None for now (may revise safe_crypt behavior in future)939 return None940 # NOTE: per issue 113, crypt() may return bytes in some odd cases.941 # assuming it should still return an ASCII hash though,942 # or there's a bigger issue at hand.943 if isinstance(result, bytes):944 result = result.decode("ascii")945 if not result or result[0] in _invalid_prefixes:946 return None947 return result948 else:949 950 #: see feature-detection in PY3 fork above951 crypt_accepts_bytes = True952 953 # Python 2 crypt handler954 def safe_crypt(secret, hash):955 if isinstance(secret, unicode):956 secret = secret.encode("utf-8")957 if _NULL in secret:958 raise ValueError("null character in secret")959 if isinstance(hash, unicode):960 hash = hash.encode("ascii")961 with _safe_crypt_lock:962 result = _crypt(secret, hash)963 if not result:964 return None965 result = result.decode("ascii")966 if result[0] in _invalid_prefixes:967 return None968 return result969 970add_doc(safe_crypt, """Wrapper around stdlib's crypt.971 972 This is a wrapper around stdlib's :func:`!crypt.crypt`, which attempts973 to provide uniform behavior across Python 2 and 3.974 975 :arg secret:976 password, as bytes or unicode (unicode will be encoded as ``utf-8``).977 978 :arg hash:979 hash or config string, as ascii bytes or unicode.980 981 :returns:982 resulting hash as ascii unicode; or ``None`` if the password983 couldn't be hashed due to one of the issues:984 985 * :func:`crypt()` not available on platform.986 987 * Under Python 3, if *secret* is specified as bytes,988 it must be use ``utf-8`` or it can't be passed989 to :func:`crypt()`.990 991 * Some OSes will return ``None`` if they don't recognize992 the algorithm being used (though most will simply fall993 back to des-crypt).994 995 * Some OSes will return an error string if the input config996 is recognized but malformed; current code converts these to ``None``997 as well.998 """)999 1000def test_crypt(secret, hash):1001 """check if :func:`crypt.crypt` supports specific hash1002 :arg secret: password to test1003 :arg hash: known hash of password to use as reference1004 :returns: True or False1005 """1006 # safe_crypt() always returns unicode, which means that for py3,1007 # 'hash' can't be bytes, or "== hash" will never be True.1008 # under py2 unicode & str(bytes) will compare fine;1009 # so just enforcing "unicode_or_str" limitation1010 assert isinstance(hash, unicode_or_str), \1011 "hash must be unicode_or_str, got %s" % type(hash)1012 assert hash, "hash must be non-empty"1013 return safe_crypt(secret, hash) == hash1014 1015timer = timeit.default_timer1016# legacy alias, will be removed in passlib 2.01017tick = timer1018 1019def parse_version(source):1020 """helper to parse version string"""1021 m = re.search(r"(\d+(?:\.\d+)+)", source)1022 if m:1023 return tuple(int(elem) for elem in m.group(1).split("."))1024 return None1025 1026#=============================================================================1027# randomness1028#=============================================================================1029 1030#------------------------------------------------------------------------1031# setup rng for generating salts1032#------------------------------------------------------------------------1033 1034# NOTE:1035# generating salts (e.g. h64_gensalt, below) doesn't require cryptographically1036# strong randomness. it just requires enough range of possible outputs1037# that making a rainbow table is too costly. so it should be ok to1038# fall back on python's builtin mersenne twister prng, as long as it's seeded each time1039# this module is imported, using a couple of minor entropy sources.1040 1041try:1042 os.urandom(1)1043 has_urandom = True1044except NotImplementedError: # pragma: no cover1045 has_urandom = False1046 1047def genseed(value=None):1048 """generate prng seed value from system resources"""1049 from hashlib import sha5121050 if hasattr(value, "getstate") and hasattr(value, "getrandbits"):1051 # caller passed in RNG as seed value1052 try:1053 value = value.getstate()1054 except NotImplementedError:1055 # this method throws error for e.g. SystemRandom instances,1056 # so fall back to extracting 4k of state1057 value = value.getrandbits(1 << 15)1058 text = u("%s %s %s %.15f %.15f %s") % (1059 # if caller specified a seed value, mix it in1060 value,1061 1062 # add current process id1063 # NOTE: not available in some environments, e.g. GAE1064 os.getpid() if hasattr(os, "getpid") else None,1065 1066 # id of a freshly created object.1067 # (at least 1 byte of which should be hard to predict)1068 id(object()),1069 1070 # the current time, to whatever precision os uses1071 time.time(),1072 tick(),1073 1074 # if urandom available, might as well mix some bytes in.1075 os.urandom(32).decode("latin-1") if has_urandom else 0,1076 )1077 # hash it all up and return it as int/long1078 return int(sha512(text.encode("utf-8")).hexdigest(), 16)1079 1080if has_urandom:1081 rng = random.SystemRandom()1082else: # pragma: no cover -- runtime detection1083 # NOTE: to reseed use ``rng.seed(genseed(rng))``1084 # XXX: could reseed on every call1085 rng = random.Random(genseed())1086 1087#------------------------------------------------------------------------1088# some rng helpers1089#------------------------------------------------------------------------1090def getrandbytes(rng, count):1091 """return byte-string containing *count* number of randomly generated bytes, using specified rng"""1092 # NOTE: would be nice if this was present in stdlib Random class1093 1094 ###just in case rng provides this...1095 ##meth = getattr(rng, "getrandbytes", None)1096 ##if meth:1097 ## return meth(count)1098 1099 if not count:1100 return _BEMPTY1101 def helper():1102 # XXX: break into chunks for large number of bits?1103 value = rng.getrandbits(count<<3)1104 i = 01105 while i < count:1106 yield value & 0xff1107 value >>= 31108 i += 11109 return join_byte_values(helper())1110 1111def getrandstr(rng, charset, count):1112 """return string containing *count* number of chars/bytes, whose elements are drawn from specified charset, using specified rng"""1113 # NOTE: tests determined this is 4x faster than rng.sample(),1114 # which is why that's not being used here.1115 1116 # check alphabet & count1117 if count < 0:1118 raise ValueError("count must be >= 0")1119 letters = len(charset)1120 if letters == 0:1121 raise ValueError("alphabet must not be empty")1122 if letters == 1:1123 return charset * count1124 1125 # get random value, and write out to buffer1126 def helper():1127 # XXX: break into chunks for large number of letters?1128 value = rng.randrange(0, letters**count)1129 i = 01130 while i < count:1131 yield charset[value % letters]1132 value //= letters1133 i += 11134 1135 if isinstance(charset, unicode):1136 return join_unicode(helper())1137 else:1138 return join_byte_elems(helper())1139 1140_52charset = '2346789ABCDEFGHJKMNPQRTUVWXYZabcdefghjkmnpqrstuvwxyz'1141 1142@deprecated_function(deprecated="1.7", removed="2.0",1143 replacement="passlib.pwd.genword() / passlib.pwd.genphrase()")1144def generate_password(size=10, charset=_52charset):1145 """generate random password using given length & charset1146 1147 :param size:1148 size of password.1149 1150 :param charset:1151 optional string specified set of characters to draw from.1152 1153 the default charset contains all normal alphanumeric characters,1154 except for the characters ``1IiLl0OoS5``, which were omitted1155 due to their visual similarity.1156 1157 :returns: :class:`!str` containing randomly generated password.1158 1159 .. note::1160 1161 Using the default character set, on a OS with :class:`!SystemRandom` support,1162 this function should generate passwords with 5.7 bits of entropy per character.1163 """1164 return getrandstr(rng, charset, size)1165 1166#=============================================================================1167# object type / interface tests1168#=============================================================================1169_handler_attrs = (1170 "name",1171 "setting_kwds", "context_kwds",1172 "verify", "hash", "identify",1173 )1174 1175def is_crypt_handler(obj):1176 """check if object follows the :ref:`password-hash-api`"""1177 # XXX: change to use isinstance(obj, PasswordHash) under py26+?1178 return all(hasattr(obj, name) for name in _handler_attrs)1179 1180_context_attrs = (1181 "needs_update",1182 "genconfig", "genhash",1183 "verify", "encrypt", "identify",1184 )1185 1186def is_crypt_context(obj):1187 """check if object appears to be a :class:`~passlib.context.CryptContext` instance"""1188 # XXX: change to use isinstance(obj, CryptContext)?1189 return all(hasattr(obj, name) for name in _context_attrs)1190 1191##def has_many_backends(handler):1192## "check if handler provides multiple baceknds"1193## # NOTE: should also provide get_backend(), .has_backend(), and .backends attr1194## return hasattr(handler, "set_backend")1195 1196def has_rounds_info(handler):1197 """check if handler provides the optional :ref:`rounds information <rounds-attributes>` attributes"""1198 return ('rounds' in handler.setting_kwds and1199 getattr(handler, "min_rounds", None) is not None)1200 