codekingpro/portable-devtools
114k
1"""passlib.handler - code for implementing handlers, and global registry for handlers"""2#=============================================================================3# imports4#=============================================================================5from __future__ import with_statement6# core7import inspect8import logging; log = logging.getLogger(__name__)9import math10import threading11from warnings import warn12# site13# pkg14import passlib.exc as exc, passlib.ifc as ifc15from passlib.exc import MissingBackendError, PasslibConfigWarning, \16 PasslibHashWarning17from passlib.ifc import PasswordHash18from passlib.registry import get_crypt_handler19from passlib.utils import (20 consteq, getrandstr, getrandbytes,21 rng, to_native_str,22 is_crypt_handler, to_unicode,23 MAX_PASSWORD_SIZE, accepts_keyword, as_bool,24 update_mixin_classes)25from passlib.utils.binary import (26 BASE64_CHARS, HASH64_CHARS, PADDED_BASE64_CHARS,27 HEX_CHARS, UPPER_HEX_CHARS, LOWER_HEX_CHARS,28 ALL_BYTE_VALUES,29)30from passlib.utils.compat import join_byte_values, irange, u, native_string_types, \31 uascii_to_str, join_unicode, unicode, str_to_uascii, \32 join_unicode, unicode_or_bytes_types, PY2, int_types33from passlib.utils.decor import classproperty, deprecated_method34# local35__all__ = [36 # helpers for implementing MCF handlers37 'parse_mc2',38 'parse_mc3',39 'render_mc2',40 'render_mc3',41 42 # framework for implementing handlers43 'GenericHandler',44 'StaticHandler',45 'HasUserContext',46 'HasRawChecksum',47 'HasManyIdents',48 'HasSalt',49 'HasRawSalt',50 'HasRounds',51 'HasManyBackends',52 53 # other helpers54 'PrefixWrapper',55 56 # TODO: a bunch of other things are commonly assumed in this namespace57 # (e.g. HEX_CHARS etc); need to audit uses and update this list.58]59 60#=============================================================================61# constants62#=============================================================================63 64# deprecated aliases - will be removed after passlib 1.865H64_CHARS = HASH64_CHARS66B64_CHARS = BASE64_CHARS67PADDED_B64_CHARS = PADDED_BASE64_CHARS68UC_HEX_CHARS = UPPER_HEX_CHARS69LC_HEX_CHARS = LOWER_HEX_CHARS70 71#=============================================================================72# support functions73#=============================================================================74def _bitsize(count, chars):75 """helper for bitsize() methods"""76 if chars and count:77 import math78 return int(count * math.log(len(chars), 2))79 else:80 return 081 82def guess_app_stacklevel(start=1):83 """84 try to guess stacklevel for application warning.85 looks for first frame not part of passlib.86 """87 frame = inspect.currentframe()88 count = -start89 try:90 while frame:91 name = frame.f_globals.get('__name__', "")92 if name.startswith("passlib.tests.") or not name.startswith("passlib."):93 return max(1, count)94 count += 195 frame = frame.f_back96 return start97 finally:98 del frame99 100def warn_hash_settings_deprecation(handler, kwds):101 warn("passing settings to %(handler)s.hash() is deprecated, and won't be supported in Passlib 2.0; "102 "use '%(handler)s.using(**settings).hash(secret)' instead" % dict(handler=handler.name),103 DeprecationWarning, stacklevel=guess_app_stacklevel(2))104 105def extract_settings_kwds(handler, kwds):106 """107 helper to extract settings kwds from mix of context & settings kwds.108 pops settings keys from kwds, returns them as a dict.109 """110 context_keys = set(handler.context_kwds)111 return dict((key, kwds.pop(key)) for key in list(kwds) if key not in context_keys)112 113#=============================================================================114# parsing helpers115#=============================================================================116_UDOLLAR = u("$")117_UZERO = u("0")118 119def validate_secret(secret):120 """ensure secret has correct type & size"""121 if not isinstance(secret, unicode_or_bytes_types):122 raise exc.ExpectedStringError(secret, "secret")123 if len(secret) > MAX_PASSWORD_SIZE:124 raise exc.PasswordSizeError(MAX_PASSWORD_SIZE)125 126def to_unicode_for_identify(hash):127 """convert hash to unicode for identify method"""128 if isinstance(hash, unicode):129 return hash130 elif isinstance(hash, bytes):131 # try as utf-8, but if it fails, use foolproof latin-1,132 # since we don't really care about non-ascii chars133 # when running identify.134 try:135 return hash.decode("utf-8")136 except UnicodeDecodeError:137 return hash.decode("latin-1")138 else:139 raise exc.ExpectedStringError(hash, "hash")140 141def parse_mc2(hash, prefix, sep=_UDOLLAR, handler=None):142 """parse hash using 2-part modular crypt format.143 144 this expects a hash of the format :samp:`{prefix}{salt}[${checksum}]`,145 such as md5_crypt, and parses it into salt / checksum portions.146 147 :arg hash: the hash to parse (bytes or unicode)148 :arg prefix: the identifying prefix (unicode)149 :param sep: field separator (unicode, defaults to ``$``).150 :param handler: handler class to pass to error constructors.151 152 :returns:153 a ``(salt, chk | None)`` tuple.154 """155 # detect prefix156 hash = to_unicode(hash, "ascii", "hash")157 assert isinstance(prefix, unicode)158 if not hash.startswith(prefix):159 raise exc.InvalidHashError(handler)160 161 # parse 2-part hash or 1-part config string162 assert isinstance(sep, unicode)163 parts = hash[len(prefix):].split(sep)164 if len(parts) == 2:165 salt, chk = parts166 return salt, chk or None167 elif len(parts) == 1:168 return parts[0], None169 else:170 raise exc.MalformedHashError(handler)171 172def parse_mc3(hash, prefix, sep=_UDOLLAR, rounds_base=10,173 default_rounds=None, handler=None):174 """parse hash using 3-part modular crypt format.175 176 this expects a hash of the format :samp:`{prefix}[{rounds}]${salt}[${checksum}]`,177 such as sha1_crypt, and parses it into rounds / salt / checksum portions.178 tries to convert the rounds to an integer,179 and throws error if it has zero-padding.180 181 :arg hash: the hash to parse (bytes or unicode)182 :arg prefix: the identifying prefix (unicode)183 :param sep: field separator (unicode, defaults to ``$``).184 :param rounds_base:185 the numeric base the rounds are encoded in (defaults to base 10).186 :param default_rounds:187 the default rounds value to return if the rounds field was omitted.188 if this is ``None`` (the default), the rounds field is *required*.189 :param handler: handler class to pass to error constructors.190 191 :returns:192 a ``(rounds : int, salt, chk | None)`` tuple.193 """194 # detect prefix195 hash = to_unicode(hash, "ascii", "hash")196 assert isinstance(prefix, unicode)197 if not hash.startswith(prefix):198 raise exc.InvalidHashError(handler)199 200 # parse 3-part hash or 2-part config string201 assert isinstance(sep, unicode)202 parts = hash[len(prefix):].split(sep)203 if len(parts) == 3:204 rounds, salt, chk = parts205 elif len(parts) == 2:206 rounds, salt = parts207 chk = None208 else:209 raise exc.MalformedHashError(handler)210 211 # validate & parse rounds portion212 if rounds.startswith(_UZERO) and rounds != _UZERO:213 raise exc.ZeroPaddedRoundsError(handler)214 elif rounds:215 rounds = int(rounds, rounds_base)216 elif default_rounds is None:217 raise exc.MalformedHashError(handler, "empty rounds field")218 else:219 rounds = default_rounds220 221 # return result222 return rounds, salt, chk or None223 224# def parse_mc3_long(hash, prefix, sep=_UDOLLAR, handler=None):225# """226# parse hash using 3-part modular crypt format,227# with complex settings string instead of simple rounds.228# otherwise works same as :func:`parse_mc3`229# """230# # detect prefix231# hash = to_unicode(hash, "ascii", "hash")232# assert isinstance(prefix, unicode)233# if not hash.startswith(prefix):234# raise exc.InvalidHashError(handler)235#236# # parse 3-part hash or 2-part config string237# assert isinstance(sep, unicode)238# parts = hash[len(prefix):].split(sep)239# if len(parts) == 3:240# return parts241# elif len(parts) == 2:242# settings, salt = parts243# return settings, salt, None244# else:245# raise exc.MalformedHashError(handler)246 247def parse_int(source, base=10, default=None, param="value", handler=None):248 """249 helper to parse an integer config field250 251 :arg source: unicode source string252 :param base: numeric base253 :param default: optional default if source is empty254 :param param: name of variable, for error msgs255 :param handler: handler class, for error msgs256 """257 if source.startswith(_UZERO) and source != _UZERO:258 raise exc.MalformedHashError(handler, "zero-padded %s field" % param)259 elif source:260 return int(source, base)261 elif default is None:262 raise exc.MalformedHashError(handler, "empty %s field" % param)263 else:264 return default265 266#=============================================================================267# formatting helpers268#=============================================================================269def render_mc2(ident, salt, checksum, sep=u("$")):270 """format hash using 2-part modular crypt format; inverse of parse_mc2()271 272 returns native string with format :samp:`{ident}{salt}[${checksum}]`,273 such as used by md5_crypt.274 275 :arg ident: identifier prefix (unicode)276 :arg salt: encoded salt (unicode)277 :arg checksum: encoded checksum (unicode or None)278 :param sep: separator char (unicode, defaults to ``$``)279 280 :returns:281 config or hash (native str)282 """283 if checksum:284 parts = [ident, salt, sep, checksum]285 else:286 parts = [ident, salt]287 return uascii_to_str(join_unicode(parts))288 289def render_mc3(ident, rounds, salt, checksum, sep=u("$"), rounds_base=10):290 """format hash using 3-part modular crypt format; inverse of parse_mc3()291 292 returns native string with format :samp:`{ident}[{rounds}$]{salt}[${checksum}]`,293 such as used by sha1_crypt.294 295 :arg ident: identifier prefix (unicode)296 :arg rounds: rounds field (int or None)297 :arg salt: encoded salt (unicode)298 :arg checksum: encoded checksum (unicode or None)299 :param sep: separator char (unicode, defaults to ``$``)300 :param rounds_base: base to encode rounds value (defaults to base 10)301 302 :returns:303 config or hash (native str)304 """305 if rounds is None:306 rounds = u('')307 elif rounds_base == 16:308 rounds = u("%x") % rounds309 else:310 assert rounds_base == 10311 rounds = unicode(rounds)312 if checksum:313 parts = [ident, rounds, sep, salt, sep, checksum]314 else:315 parts = [ident, rounds, sep, salt]316 return uascii_to_str(join_unicode(parts))317 318 319def mask_value(value, show=4, pct=0.125, char=u"*"):320 """321 helper to mask contents of sensitive field.322 323 :param value:324 raw value (str, bytes, etc)325 326 :param show:327 max # of characters to remain visible328 329 :param pct:330 don't show more than this % of input.331 332 :param char:333 character to use for masking334 335 :rtype: str | None336 """337 if value is None:338 return None339 if not isinstance(value, unicode):340 if isinstance(value, bytes):341 from passlib.utils.binary import ab64_encode342 value = ab64_encode(value).decode("ascii")343 else:344 value = unicode(value)345 size = len(value)346 show = min(show, int(size * pct))347 return value[:show] + char * (size - show)348 349#=============================================================================350# parameter helpers351#=============================================================================352 353def validate_default_value(handler, default, norm, param="value"):354 """355 assert helper that quickly validates default value.356 designed to get out of the way and reduce overhead when asserts are stripped.357 """358 assert default is not None, "%s lacks default %s" % (handler.name, param)359 assert norm(default) == default, "%s: invalid default %s: %r" % (handler.name, param, default)360 return True361 362def norm_integer(handler, value, min=1, max=None, # *363 param="value", relaxed=False):364 """365 helper to normalize and validate an integer value (e.g. rounds, salt_size)366 367 :arg value: value provided to constructor368 :arg default: default value if none provided. if set to ``None``, value is required.369 :arg param: name of parameter (xxx: move to first arg?)370 :param min: minimum value (defaults to 1)371 :param max: maximum value (default ``None`` means no maximum)372 :returns: validated value373 """374 # check type375 if not isinstance(value, int_types):376 raise exc.ExpectedTypeError(value, "integer", param)377 378 # check minimum379 if value < min:380 msg = "%s: %s (%d) is too low, must be at least %d" % (handler.name, param, value, min)381 if relaxed:382 warn(msg, exc.PasslibHashWarning)383 value = min384 else:385 raise ValueError(msg)386 387 # check maximum388 if max and value > max:389 msg = "%s: %s (%d) is too large, cannot be more than %d" % (handler.name, param, value, max)390 if relaxed:391 warn(msg, exc.PasslibHashWarning)392 value = max393 else:394 raise ValueError(msg)395 396 return value397 398#=============================================================================399# MinimalHandler400#=============================================================================401class MinimalHandler(PasswordHash):402 """403 helper class for implementing hash handlers.404 provides nothing besides a base implementation of the .using() subclass constructor.405 """406 #===================================================================407 # class attr408 #===================================================================409 410 #: private flag used by using() constructor to detect if this is already a subclass.411 _configured = False412 413 #===================================================================414 # configuration interface415 #===================================================================416 417 @classmethod418 def using(cls, relaxed=False):419 # NOTE: this provides the base implementation, which takes care of420 # creating the newly configured class. Mixins and subclasses421 # should wrap this, and modify the returned class to suit their options.422 # NOTE: 'relaxed' keyword is ignored here, but parsed so that subclasses423 # can check for it as argument, and modify their parsing behavior accordingly.424 name = cls.__name__425 if not cls._configured:426 # TODO: straighten out class naming, repr, and .name attr427 name = "<customized %s hasher>" % name428 return type(name, (cls,), dict(__module__=cls.__module__, _configured=True))429 430 #===================================================================431 # eoc432 #===================================================================433 434class TruncateMixin(MinimalHandler):435 """436 PasswordHash mixin which provides a method437 that will check if secret would be truncated,438 and can be configured to throw an error.439 440 .. warning::441 442 Hashers using this mixin will generally need to override443 the default PasswordHash.truncate_error policy of "True",444 and will similarly want to override .truncate_verify_reject as well.445 446 TODO: This should be done explicitly, but for now this mixin sets447 these flags implicitly.448 """449 450 truncate_error = False451 truncate_verify_reject = False452 453 @classmethod454 def using(cls, truncate_error=None, **kwds):455 subcls = super(TruncateMixin, cls).using(**kwds)456 if truncate_error is not None:457 truncate_error = as_bool(truncate_error, param="truncate_error")458 if truncate_error is not None:459 subcls.truncate_error = truncate_error460 return subcls461 462 @classmethod463 def _check_truncate_policy(cls, secret):464 """465 make sure secret won't be truncated.466 NOTE: this should only be called for .hash(), not for .verify(),467 which should honor the .truncate_verify_reject policy.468 """469 assert cls.truncate_size is not None, "truncate_size must be set by subclass"470 if cls.truncate_error and len(secret) > cls.truncate_size:471 raise exc.PasswordTruncateError(cls)472 473#=============================================================================474# GenericHandler475#=============================================================================476class GenericHandler(MinimalHandler):477 """helper class for implementing hash handlers.478 479 GenericHandler-derived classes will have (at least) the following480 constructor options, though others may be added by mixins481 and by the class itself:482 483 :param checksum:484 this should contain the digest portion of a485 parsed hash (mainly provided when the constructor is called486 by :meth:`from_string()`).487 defaults to ``None``.488 489 :param use_defaults:490 If ``False`` (the default), a :exc:`TypeError` should be thrown491 if any settings required by the handler were not explicitly provided.492 493 If ``True``, the handler should attempt to provide a default for any494 missing values. This means generate missing salts, fill in default495 cost parameters, etc.496 497 This is typically only set to ``True`` when the constructor498 is called by :meth:`hash`, allowing user-provided values499 to be handled in a more permissive manner.500 501 :param relaxed:502 If ``False`` (the default), a :exc:`ValueError` should be thrown503 if any settings are out of bounds or otherwise invalid.504 505 If ``True``, they should be corrected if possible, and a warning506 issue. If not possible, only then should an error be raised.507 (e.g. under ``relaxed=True``, rounds values will be clamped508 to min/max rounds).509 510 This is mainly used when parsing the config strings of certain511 hashes, whose specifications implementations to be tolerant512 of incorrect values in salt strings.513 514 Class Attributes515 ================516 517 .. attribute:: ident518 519 [optional]520 If this attribute is filled in, the default :meth:`identify` method will use521 it as a identifying prefix that can be used to recognize instances of this handler's522 hash. Filling this out is recommended for speed.523 524 This should be a unicode str.525 526 .. attribute:: _hash_regex527 528 [optional]529 If this attribute is filled in, the default :meth:`identify` method530 will use it to recognize instances of the hash. If :attr:`ident`531 is specified, this will be ignored.532 533 This should be a unique regex object.534 535 .. attribute:: checksum_size536 537 [optional]538 Specifies the number of characters that should be expected in the checksum string.539 If omitted, no check will be performed.540 541 .. attribute:: checksum_chars542 543 [optional]544 A string listing all the characters allowed in the checksum string.545 If omitted, no check will be performed.546 547 This should be a unicode str.548 549 .. attribute:: _stub_checksum550 551 Placeholder checksum that will be used by genconfig()552 in lieu of actually generating a hash for the empty string.553 This should be a string of the same datatype as :attr:`checksum`.554 555 Instance Attributes556 ===================557 .. attribute:: checksum558 559 The checksum string provided to the constructor (after passing it560 through :meth:`_norm_checksum`).561 562 Required Subclass Methods563 =========================564 The following methods must be provided by handler subclass:565 566 .. automethod:: from_string567 .. automethod:: to_string568 .. automethod:: _calc_checksum569 570 Default Methods571 ===============572 The following methods have default implementations that should work for573 most cases, though they may be overridden if the hash subclass needs to:574 575 .. automethod:: _norm_checksum576 577 .. automethod:: genconfig578 .. automethod:: genhash579 .. automethod:: identify580 .. automethod:: hash581 .. automethod:: verify582 """583 584 #===================================================================585 # class attr586 #===================================================================587 # this must be provided by the actual class.588 setting_kwds = None589 590 # providing default since most classes don't use this at all.591 context_kwds = ()592 593 # optional prefix that uniquely identifies hash594 ident = None595 596 # optional regexp for recognizing hashes,597 # used by default identify() if .ident isn't specified.598 _hash_regex = None599 600 # if specified, _norm_checksum will require this length601 checksum_size = None602 603 # if specified, _norm_checksum() will validate this604 checksum_chars = None605 606 # private flag used by HasRawChecksum607 _checksum_is_bytes = False608 609 #===================================================================610 # instance attrs611 #===================================================================612 checksum = None # stores checksum613# use_defaults = False # whether _norm_xxx() funcs should fill in defaults.614# relaxed = False # when _norm_xxx() funcs should be strict about inputs615 616 #===================================================================617 # init618 #===================================================================619 def __init__(self, checksum=None, use_defaults=False, **kwds):620 self.use_defaults = use_defaults621 super(GenericHandler, self).__init__(**kwds)622 if checksum is not None:623 # XXX: do we need to set .relaxed for checksum coercion?624 self.checksum = self._norm_checksum(checksum)625 626 # NOTE: would like to make this classmethod, but fshp checksum size627 # is dependant on .variant, so leaving this as instance method.628 def _norm_checksum(self, checksum, relaxed=False):629 """validates checksum keyword against class requirements,630 returns normalized version of checksum.631 """632 # NOTE: by default this code assumes checksum should be unicode.633 # For classes where the checksum is raw bytes, the HasRawChecksum sets634 # the _checksum_is_bytes flag which alters various code paths below.635 636 # normalize to bytes / unicode637 raw = self._checksum_is_bytes638 if raw:639 # NOTE: no clear route to reasonably convert unicode -> raw bytes,640 # so 'relaxed' does nothing here641 if not isinstance(checksum, bytes):642 raise exc.ExpectedTypeError(checksum, "bytes", "checksum")643 644 elif not isinstance(checksum, unicode):645 if isinstance(checksum, bytes) and relaxed:646 warn("checksum should be unicode, not bytes", PasslibHashWarning)647 checksum = checksum.decode("ascii")648 else:649 raise exc.ExpectedTypeError(checksum, "unicode", "checksum")650 651 # check size652 cc = self.checksum_size653 if cc and len(checksum) != cc:654 raise exc.ChecksumSizeError(self, raw=raw)655 656 # check charset657 if not raw:658 cs = self.checksum_chars659 if cs and any(c not in cs for c in checksum):660 raise ValueError("invalid characters in %s checksum" % (self.name,))661 662 return checksum663 664 #===================================================================665 # password hash api - formatting interface666 #===================================================================667 @classmethod668 def identify(cls, hash):669 # NOTE: subclasses may wish to use faster / simpler identify,670 # and raise value errors only when an invalid (but identifiable)671 # string is parsed672 hash = to_unicode_for_identify(hash)673 if not hash:674 return False675 676 # does class specify a known unique prefix to look for?677 ident = cls.ident678 if ident is not None:679 return hash.startswith(ident)680 681 # does class provide a regexp to use?682 pat = cls._hash_regex683 if pat is not None:684 return pat.match(hash) is not None685 686 # as fallback, try to parse hash, and see if we succeed.687 # inefficient, but works for most cases.688 try:689 cls.from_string(hash)690 return True691 except ValueError:692 return False693 694 @classmethod695 def from_string(cls, hash, **context): # pragma: no cover696 r"""697 return parsed instance from hash/configuration string698 699 :param \\*\\*context:700 context keywords to pass to constructor (if applicable).701 702 :raises ValueError: if hash is incorrectly formatted703 704 :returns:705 hash parsed into components,706 for formatting / calculating checksum.707 """708 raise NotImplementedError("%s must implement from_string()" % (cls,))709 710 def to_string(self): # pragma: no cover711 """render instance to hash or configuration string712 713 :returns:714 hash string with salt & digest included.715 716 should return native string type (ascii-bytes under python 2,717 unicode under python 3)718 """719 raise NotImplementedError("%s must implement from_string()" % (self.__class__,))720 721 #===================================================================722 # checksum generation723 #===================================================================724 725 # NOTE: this is only used by genconfig(), and will be removed in passlib 2.0726 @property727 def _stub_checksum(self):728 """729 placeholder used by default .genconfig() so it can avoid expense of calculating digest.730 """731 # used fixed string if available732 if self.checksum_size:733 if self._checksum_is_bytes:734 return b'\x00' * self.checksum_size735 if self.checksum_chars:736 return self.checksum_chars[0] * self.checksum_size737 738 # hack to minimize cost of calculating real checksum739 if isinstance(self, HasRounds):740 orig = self.rounds741 self.rounds = self.min_rounds or 1742 try:743 return self._calc_checksum("")744 finally:745 self.rounds = orig746 747 # final fallback, generate a real checksum748 return self._calc_checksum("")749 750 def _calc_checksum(self, secret): # pragma: no cover751 """given secret; calcuate and return encoded checksum portion of hash752 string, taking config from object state753 754 calc checksum implementations may assume secret is always755 either unicode or bytes, checks are performed by verify/etc.756 """757 raise NotImplementedError("%s must implement _calc_checksum()" %758 (self.__class__,))759 760 #===================================================================761 #'application' interface (default implementation)762 #===================================================================763 764 @classmethod765 def hash(cls, secret, **kwds):766 if kwds:767 # Deprecating passing any settings keywords via .hash() as of passlib 1.7; everything768 # should use .using().hash() instead. If any keywords are specified, presume they're769 # context keywords by default (the common case), and extract out any settings kwds.770 # Support for passing settings via .hash() will be removed in Passlib 2.0, along with771 # this block of code.772 settings = extract_settings_kwds(cls, kwds)773 if settings:774 warn_hash_settings_deprecation(cls, settings)775 return cls.using(**settings).hash(secret, **kwds)776 # NOTE: at this point, 'kwds' should just contain context_kwds subset777 validate_secret(secret)778 self = cls(use_defaults=True, **kwds)779 self.checksum = self._calc_checksum(secret)780 return self.to_string()781 782 @classmethod783 def verify(cls, secret, hash, **context):784 # NOTE: classes with multiple checksum encodings should either785 # override this method, or ensure that from_string() / _norm_checksum()786 # ensures .checksum always uses a single canonical representation.787 validate_secret(secret)788 self = cls.from_string(hash, **context)789 chk = self.checksum790 if chk is None:791 raise exc.MissingDigestError(cls)792 return consteq(self._calc_checksum(secret), chk)793 794 #===================================================================795 # legacy crypt interface796 #===================================================================797 798 @deprecated_method(deprecated="1.7", removed="2.0")799 @classmethod800 def genconfig(cls, **kwds):801 # NOTE: 'kwds' should generally always be settings, so after this completes, *should* be empty.802 settings = extract_settings_kwds(cls, kwds)803 if settings:804 return cls.using(**settings).genconfig(**kwds)805 # NOTE: this uses optional stub checksum to bypass potentially expensive digest generation,806 # when caller just wants the config string.807 self = cls(use_defaults=True, **kwds)808 self.checksum = self._stub_checksum809 return self.to_string()810 811 @deprecated_method(deprecated="1.7", removed="2.0")812 @classmethod813 def genhash(cls, secret, config, **context):814 if config is None:815 raise TypeError("config must be string")816 validate_secret(secret)817 self = cls.from_string(config, **context)818 self.checksum = self._calc_checksum(secret)819 return self.to_string()820 821 #===================================================================822 # migration interface (basde implementation)823 #===================================================================824 825 @classmethod826 def needs_update(cls, hash, secret=None, **kwds):827 # NOTE: subclasses should generally just wrap _calc_needs_update()828 # to check their particular keywords.829 self = cls.from_string(hash)830 assert isinstance(self, cls)831 return self._calc_needs_update(secret=secret, **kwds)832 833 def _calc_needs_update(self, secret=None):834 """835 internal helper for :meth:`needs_update`.836 """837 # NOTE: this just provides a stub, subclasses & mixins838 # should override this with their own tests.839 return False840 841 #===================================================================842 # experimental - the following methods are not finished or tested,843 # but way work correctly for some hashes844 #===================================================================845 846 #: internal helper for forcing settings to be included, even if default matches847 _always_parse_settings = ()848 849 #: internal helper for excluding certain setting_kwds from parsehash() result850 _unparsed_settings = ("salt_size", "relaxed")851 852 #: parsehash() keys that need to be sanitized853 _unsafe_settings = ("salt", "checksum")854 855 @classproperty856 def _parsed_settings(cls):857 """858 helper for :meth:`parsehash` --859 returns list of attributes which should be extracted by parse_hash() from hasher object.860 861 default implementation just takes setting_kwds, and excludes _unparsed_settings862 """863 return tuple(key for key in cls.setting_kwds if key not in cls._unparsed_settings)864 865 @classmethod866 def parsehash(cls, hash, checksum=True, sanitize=False):867 """[experimental method] parse hash into dictionary of settings.868 869 this essentially acts as the inverse of :meth:`hash`: for most870 cases, if ``hash = cls.hash(secret, **opts)``, then871 ``cls.parsehash(hash)`` will return a dict matching the original options872 (with the extra keyword *checksum*).873 874 this method may not work correctly for all hashes,875 and may not be available on some few. its interface may876 change in future releases, if it's kept around at all.877 878 :arg hash: hash to parse879 :param checksum: include checksum keyword? (defaults to True)880 :param sanitize: mask data for sensitive fields? (defaults to False)881 """882 # FIXME: this may not work for hashes with non-standard settings.883 # XXX: how should this handle checksum/salt encoding?884 # need to work that out for hash() anyways.885 self = cls.from_string(hash)886 # XXX: could split next few lines out as self._parsehash() for subclassing887 # XXX: could try to resolve ident/variant to publically suitable alias.888 # XXX: for v1.8, consider making "always" the default policy, and compare to class default889 # only for whitelisted attrs? or make this whole method obsolete by reworking890 # so "hasher" object & it's attrs are public?891 UNSET = object()892 always = self._always_parse_settings893 kwds = dict((key, getattr(self, key)) for key in self._parsed_settings894 if key in always or getattr(self, key) != getattr(cls, key, UNSET))895 if checksum and self.checksum is not None:896 kwds['checksum'] = self.checksum897 if sanitize:898 if sanitize is True:899 sanitize = mask_value900 for key in cls._unsafe_settings:901 if key in kwds:902 kwds[key] = sanitize(kwds[key])903 return kwds904 905 @classmethod906 def bitsize(cls, **kwds):907 """[experimental method] return info about bitsizes of hash"""908 try:909 info = super(GenericHandler, cls).bitsize(**kwds)910 except AttributeError:911 info = {}912 cc = ALL_BYTE_VALUES if cls._checksum_is_bytes else cls.checksum_chars913 if cls.checksum_size and cc:914 # FIXME: this may overestimate size due to padding bits (e.g. bcrypt)915 # FIXME: this will be off by 1 for case-insensitive hashes.916 info['checksum'] = _bitsize(cls.checksum_size, cc)917 return info918 919 #===================================================================920 # eoc921 #===================================================================922 923class StaticHandler(GenericHandler):924 """GenericHandler mixin for classes which have no settings.925 926 This mixin assumes the entirety of the hash ise stored in the927 :attr:`checksum` attribute; that the hash has no rounds, salt,928 etc. This class provides the following:929 930 * a default :meth:`genconfig` that always returns None.931 * a default :meth:`from_string` and :meth:`to_string`932 that store the entire hash within :attr:`checksum`,933 after optionally stripping a constant prefix.934 935 All that is required by subclasses is an implementation of936 the :meth:`_calc_checksum` method.937 """938 # TODO: document _norm_hash()939 940 setting_kwds = ()941 942 # optional constant prefix subclasses can specify943 _hash_prefix = u("")944 945 @classmethod946 def from_string(cls, hash, **context):947 # default from_string() which strips optional prefix,948 # and passes rest unchanged as checksum value.949 hash = to_unicode(hash, "ascii", "hash")950 hash = cls._norm_hash(hash)951 # could enable this for extra strictness952 ##pat = cls._hash_regex953 ##if pat and pat.match(hash) is None:954 ## raise ValueError("not a valid %s hash" % (cls.name,))955 prefix = cls._hash_prefix956 if prefix:957 if hash.startswith(prefix):958 hash = hash[len(prefix):]959 else:960 raise exc.InvalidHashError(cls)961 return cls(checksum=hash, **context)962 963 @classmethod964 def _norm_hash(cls, hash):965 """helper for subclasses to normalize case if needed"""966 return hash967 968 def to_string(self):969 return uascii_to_str(self._hash_prefix + self.checksum)970 971 # per-subclass: stores dynamically created subclass used by _calc_checksum() stub972 __cc_compat_hack = None973 974 def _calc_checksum(self, secret):975 """given secret; calcuate and return encoded checksum portion of hash976 string, taking config from object state977 """978 # NOTE: prior to 1.6, StaticHandler required classes implement genhash979 # instead of this method. so if we reach here, we try calling genhash.980 # if that succeeds, we issue deprecation warning. if it fails,981 # we'll just recurse back to here, but in a different instance.982 # so before we call genhash, we create a subclass which handles983 # throwing the NotImplementedError.984 cls = self.__class__985 assert cls.__module__ != __name__986 wrapper_cls = cls.__cc_compat_hack987 if wrapper_cls is None:988 def inner(self, secret):989 raise NotImplementedError("%s must implement _calc_checksum()" %990 (cls,))991 wrapper_cls = cls.__cc_compat_hack = type(cls.__name__ + "_wrapper",992 (cls,), dict(_calc_checksum=inner, __module__=cls.__module__))993 context = dict((k,getattr(self,k)) for k in self.context_kwds)994 # NOTE: passing 'config=None' here even though not currently allowed by ifc,995 # since it *is* allowed under the old 1.5 ifc we're checking for here.996 try:997 hash = wrapper_cls.genhash(secret, None, **context)998 except TypeError as err:999 if str(err) == "config must be string":1000 raise NotImplementedError("%s must implement _calc_checksum()" %1001 (cls,))1002 else:1003 raise1004 warn("%r should be updated to implement StaticHandler._calc_checksum() "1005 "instead of StaticHandler.genhash(), support for the latter "1006 "style will be removed in Passlib 1.8" % cls,1007 DeprecationWarning)1008 return str_to_uascii(hash)1009 1010#=============================================================================1011# GenericHandler mixin classes1012#=============================================================================1013class HasEncodingContext(GenericHandler):1014 """helper for classes which require knowledge of the encoding used"""1015 context_kwds = ("encoding",)1016 default_encoding = "utf-8"1017 1018 def __init__(self, encoding=None, **kwds):1019 super(HasEncodingContext, self).__init__(**kwds)1020 self.encoding = encoding or self.default_encoding1021 1022class HasUserContext(GenericHandler):1023 """helper for classes which require a user context keyword"""1024 context_kwds = ("user",)1025 1026 def __init__(self, user=None, **kwds):1027 super(HasUserContext, self).__init__(**kwds)1028 self.user = user1029 1030 # XXX: would like to validate user input here, but calls to from_string()1031 # which lack context keywords would then fail; so leaving code per-handler.1032 1033 # wrap funcs to accept 'user' as positional arg for ease of use.1034 @classmethod1035 def hash(cls, secret, user=None, **context):1036 return super(HasUserContext, cls).hash(secret, user=user, **context)1037 1038 @classmethod1039 def verify(cls, secret, hash, user=None, **context):1040 return super(HasUserContext, cls).verify(secret, hash, user=user, **context)1041 1042 @deprecated_method(deprecated="1.7", removed="2.0")1043 @classmethod1044 def genhash(cls, secret, config, user=None, **context):1045 return super(HasUserContext, cls).genhash(secret, config, user=user, **context)1046 1047 # XXX: how to guess the entropy of a username?1048 # most of these hashes are for a system (e.g. Oracle)1049 # which has a few *very common* names and thus really low entropy;1050 # while the rest are slightly less predictable.1051 # need to find good reference about this.1052 ##@classmethod1053 ##def bitsize(cls, **kwds):1054 ## info = super(HasUserContext, cls).bitsize(**kwds)1055 ## info['user'] = xxx1056 ## return info1057 1058#------------------------------------------------------------------------1059# checksum mixins1060#------------------------------------------------------------------------1061class HasRawChecksum(GenericHandler):1062 """mixin for classes which work with decoded checksum bytes1063 1064 .. todo::1065 1066 document this class's usage1067 """1068 # NOTE: GenericHandler.checksum_chars is ignored by this implementation.1069 1070 # NOTE: all HasRawChecksum code is currently part of GenericHandler,1071 # using private '_checksum_is_bytes' flag.1072 # this arrangement may be changed in the future.1073 _checksum_is_bytes = True1074 1075#------------------------------------------------------------------------1076# ident mixins1077#------------------------------------------------------------------------1078class HasManyIdents(GenericHandler):1079 """mixin for hashes which use multiple prefix identifiers1080 1081 For the hashes which may use multiple identifier prefixes,1082 this mixin adds an ``ident`` keyword to constructor.1083 Any value provided is passed through the :meth:`norm_idents` method,1084 which takes care of validating the identifier,1085 as well as allowing aliases for easier specification1086 of the identifiers by the user.1087 1088 .. todo::1089 1090 document this class's usage1091 1092 Class Methods1093 =============1094 .. todo:: document using() and needs_update() options1095 """1096 1097 #===================================================================1098 # class attrs1099 #===================================================================1100 default_ident = None # should be unicode1101 ident_values = None # should be list of unicode strings1102 ident_aliases = None # should be dict of unicode -> unicode1103 # NOTE: any aliases provided to norm_ident() as bytes1104 # will have been converted to unicode before1105 # comparing against this dictionary.1106 1107 # NOTE: relying on test_06_HasManyIdents() to verify1108 # these are configured correctly.1109 1110 #===================================================================1111 # instance attrs1112 #===================================================================1113 ident = None1114 1115 #===================================================================1116 # variant constructor1117 #===================================================================1118 @classmethod1119 def using(cls, # keyword only...1120 default_ident=None, ident=None, **kwds):1121 """1122 This mixin adds support for the following :meth:`~passlib.ifc.PasswordHash.using` keywords:1123 1124 :param default_ident:1125 default identifier that will be used by resulting customized hasher.1126 1127 :param ident:1128 supported as alternate alias for **default_ident**.1129 """1130 # resolve aliases1131 if ident is not None:1132 if default_ident is not None:1133 raise TypeError("'default_ident' and 'ident' are mutually exclusive")1134 default_ident = ident1135 1136 # create subclass1137 subcls = super(HasManyIdents, cls).using(**kwds)1138 1139 # add custom default ident1140 # (NOTE: creates instance to run value through _norm_ident())1141 if default_ident is not None:1142 subcls.default_ident = cls(ident=default_ident, use_defaults=True).ident1143 return subcls1144 1145 #===================================================================1146 # init1147 #===================================================================1148 def __init__(self, ident=None, **kwds):1149 super(HasManyIdents, self).__init__(**kwds)1150 1151 # init ident1152 if ident is not None:1153 ident = self._norm_ident(ident)1154 elif self.use_defaults:1155 ident = self.default_ident1156 assert validate_default_value(self, ident, self._norm_ident, param="default_ident")1157 else:1158 raise TypeError("no ident specified")1159 self.ident = ident1160 1161 @classmethod1162 def _norm_ident(cls, ident):1163 """1164 helper which normalizes & validates 'ident' value.1165 """1166 # handle bytes1167 assert ident is not None1168 if isinstance(ident, bytes):1169 ident = ident.decode('ascii')1170 1171 # check if identifier is valid1172 iv = cls.ident_values1173 if ident in iv:1174 return ident1175 1176 # resolve aliases, and recheck against ident_values1177 ia = cls.ident_aliases1178 if ia:1179 try:1180 value = ia[ident]1181 except KeyError:1182 pass1183 else:1184 if value in iv:1185 return value1186 1187 # failure!1188 # XXX: give this it's own error type?1189 raise ValueError("invalid ident: %r" % (ident,))1190 1191 #===================================================================1192 # password hash api1193 #===================================================================1194 @classmethod1195 def identify(cls, hash):1196 hash = to_unicode_for_identify(hash)1197 return hash.startswith(cls.ident_values)1198 1199 @classmethod1200 def _parse_ident(cls, hash):