codekingpro/portable-devtools
114k
1"""passlib.handlers.argon2 -- argon2 password hash wrapper2 3References4==========5* argon26 - home: https://github.com/P-H-C/phc-winner-argon27 - whitepaper: https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf8* argon2 cffi wrapper9 - pypi: https://pypi.python.org/pypi/argon2_cffi10 - home: https://github.com/hynek/argon2_cffi11* argon2 pure python12 - pypi: https://pypi.python.org/pypi/argon2pure13 - home: https://github.com/bwesterb/argon2pure14"""15#=============================================================================16# imports17#=============================================================================18from __future__ import with_statement, absolute_import19# core20import logging21log = logging.getLogger(__name__)22import re23import types24from warnings import warn25# site26_argon2_cffi = None # loaded below27_argon2pure = None # dynamically imported by _load_backend_argon2pure()28# pkg29from passlib import exc30from passlib.crypto.digest import MAX_UINT3231from passlib.utils import classproperty, to_bytes, render_bytes32from passlib.utils.binary import b64s_encode, b64s_decode33from passlib.utils.compat import u, unicode, bascii_to_str, uascii_to_str, PY234import passlib.utils.handlers as uh35# local36__all__ = [37 "argon2",38]39 40#=============================================================================41# helpers42#=============================================================================43 44# NOTE: when adding a new argon2 hash type, need to do the following:45# * add TYPE_XXX constant, and add to ALL_TYPES46# * make sure "_backend_type_map" constructors handle it correctly for all backends47# * make sure _hash_regex & _ident_regex (below) support type string.48# * add reference vectors for testing.49 50#: argon2 type constants -- subclasses handle mapping these to backend-specific type constants.51#: (should be lowercase, to match representation in hash string)52TYPE_I = u("i")53TYPE_D = u("d")54TYPE_ID = u("id") # new 2016-10-29; passlib 1.7.2 requires backends new enough for support55 56#: list of all known types; first (supported) type will be used as default.57ALL_TYPES = (TYPE_ID, TYPE_I, TYPE_D)58ALL_TYPES_SET = set(ALL_TYPES)59 60#=============================================================================61# import argon2 package (https://pypi.python.org/pypi/argon2_cffi)62#=============================================================================63 64# import cffi package65# NOTE: we try to do this even if caller is going to use argon2pure,66# so that we can always use the libargon2 default settings when possible.67_argon2_cffi_error = None68try:69 import argon2 as _argon2_cffi70except ImportError:71 _argon2_cffi = None72else:73 if not hasattr(_argon2_cffi, "Type"):74 # they have incompatible "argon2" package installed, instead of "argon2_cffi" package.75 _argon2_cffi_error = (76 "'argon2' module points to unsupported 'argon2' pypi package; "77 "please install 'argon2-cffi' instead."78 )79 _argon2_cffi = None80 elif not hasattr(_argon2_cffi, "low_level"):81 # they have pre-v16 argon2_cffi package82 _argon2_cffi_error = "'argon2-cffi' is too old, please update to argon2_cffi >= 18.2.0"83 _argon2_cffi = None84 85# init default settings for our hasher class --86# if we have argon2_cffi >= 16.0, use their default hasher settings, otherwise use static default87if hasattr(_argon2_cffi, "PasswordHasher"):88 # use cffi's default settings89 _default_settings = _argon2_cffi.PasswordHasher()90 _default_version = _argon2_cffi.low_level.ARGON2_VERSION91else:92 # use fallback settings (for no backend, or argon2pure)93 class _DummyCffiHasher:94 """95 dummy object to use as source of defaults when argon2_cffi isn't present.96 this tries to mimic the attributes of ``argon2.PasswordHasher()`` which the rest of97 this module reads.98 99 .. note:: values last synced w/ argon2 19.2 as of 2019-11-09100 """101 time_cost = 2102 memory_cost = 512103 parallelism = 2104 salt_len = 16105 hash_len = 16106 # NOTE: "type" attribute added in argon2_cffi v18.2; but currently not reading it107 # type = _argon2_cffi.Type.ID108 109 _default_settings = _DummyCffiHasher()110 _default_version = 0x13 # v1.9111 112#=============================================================================113# handler114#=============================================================================115class _Argon2Common(uh.SubclassBackendMixin, uh.ParallelismMixin,116 uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum,117 uh.GenericHandler):118 """119 Base class which implements brunt of Argon2 code.120 This is then subclassed by the various backends,121 to override w/ backend-specific methods.122 123 When a backend is loaded, the bases of the 'argon2' class proper124 are modified to prepend the correct backend-specific subclass.125 """126 #===================================================================127 # class attrs128 #===================================================================129 130 #------------------------131 # PasswordHash132 #------------------------133 134 name = "argon2"135 setting_kwds = ("salt",136 "salt_size",137 "salt_len", # 'salt_size' alias for compat w/ argon2 package138 "rounds",139 "time_cost", # 'rounds' alias for compat w/ argon2 package140 "memory_cost",141 "parallelism",142 "digest_size",143 "hash_len", # 'digest_size' alias for compat w/ argon2 package144 "type", # the type of argon2 hash used145 )146 147 # TODO: could support the optional 'data' parameter,148 # but need to research the uses, what a more descriptive name would be,149 # and deal w/ fact that argon2_cffi 16.1 doesn't currently support it.150 # (argon2_pure does though)151 152 #------------------------153 # GenericHandler154 #------------------------155 156 # NOTE: ident -- all argon2 hashes start with "$argon2<type>$"157 # XXX: could programmaticaly generate "ident_values" string from ALL_TYPES above158 159 checksum_size = _default_settings.hash_len160 161 #: force parsing these kwds162 _always_parse_settings = uh.GenericHandler._always_parse_settings + \163 ("type",)164 165 #: exclude these kwds from parsehash() result (most are aliases for other keys)166 _unparsed_settings = uh.GenericHandler._unparsed_settings + \167 ("salt_len", "time_cost", "hash_len", "digest_size")168 169 #------------------------170 # HasSalt171 #------------------------172 default_salt_size = _default_settings.salt_len173 min_salt_size = 8174 max_salt_size = MAX_UINT32175 176 #------------------------177 # HasRounds178 # TODO: once rounds limit logic is factored out,179 # make 'rounds' and 'cost' an alias for 'time_cost'180 #------------------------181 default_rounds = _default_settings.time_cost182 min_rounds = 1183 max_rounds = MAX_UINT32184 rounds_cost = "linear"185 186 #------------------------187 # ParalleismMixin188 #------------------------189 max_parallelism = (1 << 24) - 1 # from argon2.h / ARGON2_MAX_LANES190 191 #------------------------192 # custom193 #------------------------194 195 #: max version support196 #: NOTE: this is dependant on the backend, and initialized/modified by set_backend()197 max_version = _default_version198 199 #: minimum version before needs_update() marks the hash; if None, defaults to max_version200 min_desired_version = None201 202 #: minimum valid memory_cost203 min_memory_cost = 8 # from argon2.h / ARGON2_MIN_MEMORY204 205 #: maximum number of threads (-1=unlimited);206 #: number of threads used by .hash() will be min(parallelism, max_threads)207 max_threads = -1208 209 #: global flag signalling argon2pure backend to use threads210 #: rather than subprocesses.211 pure_use_threads = False212 213 #: internal helper used to store mapping of TYPE_XXX constants -> backend-specific type constants;214 #: this is populated by _load_backend_mixin(); and used to detect which types are supported.215 #: XXX: could expose keys as class-level .supported_types property?216 _backend_type_map = {}217 218 @classproperty219 def type_values(cls):220 """221 return tuple of types supported by this backend222 223 .. versionadded:: 1.7.2224 """225 cls.get_backend() # make sure backend is loaded226 return tuple(cls._backend_type_map)227 228 #===================================================================229 # instance attrs230 #===================================================================231 232 #: argon2 hash type, one of ALL_TYPES -- class value controls the default233 #: .. versionadded:: 1.7.2234 type = TYPE_ID235 236 #: parallelism setting -- class value controls the default237 parallelism = _default_settings.parallelism238 239 #: hash version (int)240 #: NOTE: this is modified by set_backend()241 version = _default_version242 243 #: memory cost -- class value controls the default244 memory_cost = _default_settings.memory_cost245 246 @property247 def type_d(self):248 """249 flag indicating a Type D hash250 251 .. deprecated:: 1.7.2; will be removed in passlib 2.0252 """253 return self.type == TYPE_D254 255 #: optional secret data256 data = None257 258 #===================================================================259 # variant constructor260 #===================================================================261 262 @classmethod263 def using(cls, type=None, memory_cost=None, salt_len=None, time_cost=None, digest_size=None,264 checksum_size=None, hash_len=None, max_threads=None, **kwds):265 # support aliases which match argon2 naming convention266 if time_cost is not None:267 if "rounds" in kwds:268 raise TypeError("'time_cost' and 'rounds' are mutually exclusive")269 kwds['rounds'] = time_cost270 271 if salt_len is not None:272 if "salt_size" in kwds:273 raise TypeError("'salt_len' and 'salt_size' are mutually exclusive")274 kwds['salt_size'] = salt_len275 276 if hash_len is not None:277 if digest_size is not None:278 raise TypeError("'hash_len' and 'digest_size' are mutually exclusive")279 digest_size = hash_len280 281 if checksum_size is not None:282 if digest_size is not None:283 raise TypeError("'checksum_size' and 'digest_size' are mutually exclusive")284 digest_size = checksum_size285 286 # create variant287 subcls = super(_Argon2Common, cls).using(**kwds)288 289 # set type290 if type is not None:291 subcls.type = subcls._norm_type(type)292 293 # set checksum size294 relaxed = kwds.get("relaxed")295 if digest_size is not None:296 if isinstance(digest_size, uh.native_string_types):297 digest_size = int(digest_size)298 # NOTE: this isn't *really* digest size minimum, but want to enforce secure minimum.299 subcls.checksum_size = uh.norm_integer(subcls, digest_size, min=16, max=MAX_UINT32,300 param="digest_size", relaxed=relaxed)301 302 # set memory cost303 if memory_cost is not None:304 if isinstance(memory_cost, uh.native_string_types):305 memory_cost = int(memory_cost)306 subcls.memory_cost = subcls._norm_memory_cost(memory_cost, relaxed=relaxed)307 308 # validate constraints309 subcls._validate_constraints(subcls.memory_cost, subcls.parallelism)310 311 # set max threads312 if max_threads is not None:313 if isinstance(max_threads, uh.native_string_types):314 max_threads = int(max_threads)315 if max_threads < 1 and max_threads != -1:316 raise ValueError("max_threads (%d) must be -1 (unlimited), or at least 1." %317 (max_threads,))318 subcls.max_threads = max_threads319 320 return subcls321 322 @classmethod323 def _validate_constraints(cls, memory_cost, parallelism):324 # NOTE: this is used by class & instance, hence passing in via arguments.325 # could switch and make this a hybrid method.326 min_memory_cost = 8 * parallelism327 if memory_cost < min_memory_cost:328 raise ValueError("%s: memory_cost (%d) is too low, must be at least "329 "8 * parallelism (8 * %d = %d)" %330 (cls.name, memory_cost,331 parallelism, min_memory_cost))332 333 #===================================================================334 # public api335 #===================================================================336 337 #: shorter version of _hash_regex, used to quickly identify hashes338 _ident_regex = re.compile(r"^\$argon2[a-z]+\$")339 340 @classmethod341 def identify(cls, hash):342 hash = uh.to_unicode_for_identify(hash)343 return cls._ident_regex.match(hash) is not None344 345 # hash(), verify(), genhash() -- implemented by backend subclass346 347 #===================================================================348 # hash parsing / rendering349 #===================================================================350 351 # info taken from source of decode_string() function in352 # <https://github.com/P-H-C/phc-winner-argon2/blob/master/src/encoding.c>353 #354 # hash format:355 # $argon2<T>[$v=<num>]$m=<num>,t=<num>,p=<num>[,keyid=<bin>][,data=<bin>][$<bin>[$<bin>]]356 #357 # NOTE: as of 2016-6-17, the official source (above) lists the "keyid" param in the comments,358 # but the actual source of decode_string & encode_string don't mention it at all.359 # we're supporting parsing it, but throw NotImplementedError if encountered.360 #361 # sample hashes:362 # v1.0: '$argon2i$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'363 # v1.3: '$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'364 365 #: regex to parse argon hash366 _hash_regex = re.compile(br"""367 ^368 \$argon2(?P<type>[a-z]+)\$369 (?:370 v=(?P<version>\d+)371 \$372 )?373 m=(?P<memory_cost>\d+)374 ,375 t=(?P<time_cost>\d+)376 ,377 p=(?P<parallelism>\d+)378 (?:379 ,keyid=(?P<keyid>[^,$]+)380 )?381 (?:382 ,data=(?P<data>[^,$]+)383 )?384 (?:385 \$386 (?P<salt>[^$]+)387 (?:388 \$389 (?P<digest>.+)390 )?391 )?392 $393 """, re.X)394 395 @classmethod396 def from_string(cls, hash):397 # NOTE: assuming hash will be unicode, or use ascii-compatible encoding.398 # TODO: switch to working w/ str or unicode399 if isinstance(hash, unicode):400 hash = hash.encode("utf-8")401 if not isinstance(hash, bytes):402 raise exc.ExpectedStringError(hash, "hash")403 m = cls._hash_regex.match(hash)404 if not m:405 raise exc.MalformedHashError(cls)406 type, version, memory_cost, time_cost, parallelism, keyid, data, salt, digest = \407 m.group("type", "version", "memory_cost", "time_cost", "parallelism",408 "keyid", "data", "salt", "digest")409 if keyid:410 raise NotImplementedError("argon2 'keyid' parameter not supported")411 return cls(412 type=type.decode("ascii"),413 version=int(version) if version else 0x10,414 memory_cost=int(memory_cost),415 rounds=int(time_cost),416 parallelism=int(parallelism),417 salt=b64s_decode(salt) if salt else None,418 data=b64s_decode(data) if data else None,419 checksum=b64s_decode(digest) if digest else None,420 )421 422 def to_string(self):423 version = self.version424 if version == 0x10:425 vstr = ""426 else:427 vstr = "v=%d$" % version428 429 data = self.data430 if data:431 kdstr = ",data=" + bascii_to_str(b64s_encode(self.data))432 else:433 kdstr = ""434 435 # NOTE: 'keyid' param currently not supported436 return "$argon2%s$%sm=%d,t=%d,p=%d%s$%s$%s" % (437 uascii_to_str(self.type),438 vstr, 439 self.memory_cost,440 self.rounds, 441 self.parallelism,442 kdstr,443 bascii_to_str(b64s_encode(self.salt)),444 bascii_to_str(b64s_encode(self.checksum)),445 )446 447 #===================================================================448 # init449 #===================================================================450 def __init__(self, type=None, type_d=False, version=None, memory_cost=None, data=None, **kwds):451 452 # handle deprecated kwds453 if type_d:454 warn('argon2 `type_d=True` keyword is deprecated, and will be removed in passlib 2.0; '455 'please use ``type="d"`` instead')456 assert type is None457 type = TYPE_D458 459 # TODO: factor out variable checksum size support into a mixin.460 # set checksum size to specific value before _norm_checksum() is called461 checksum = kwds.get("checksum")462 if checksum is not None:463 self.checksum_size = len(checksum)464 465 # call parent466 super(_Argon2Common, self).__init__(**kwds)467 468 # init type469 if type is None:470 assert uh.validate_default_value(self, self.type, self._norm_type, param="type")471 else:472 self.type = self._norm_type(type)473 474 # init version475 if version is None:476 assert uh.validate_default_value(self, self.version, self._norm_version,477 param="version")478 else:479 self.version = self._norm_version(version)480 481 # init memory cost482 if memory_cost is None:483 assert uh.validate_default_value(self, self.memory_cost, self._norm_memory_cost,484 param="memory_cost")485 else:486 self.memory_cost = self._norm_memory_cost(memory_cost)487 488 # init data489 if data is None:490 assert self.data is None491 else:492 if not isinstance(data, bytes):493 raise uh.exc.ExpectedTypeError(data, "bytes", "data")494 self.data = data495 496 #-------------------------------------------------------------------497 # parameter guards498 #-------------------------------------------------------------------499 500 @classmethod501 def _norm_type(cls, value):502 # type check503 if not isinstance(value, unicode):504 if PY2 and isinstance(value, bytes):505 value = value.decode('ascii')506 else:507 raise uh.exc.ExpectedTypeError(value, "str", "type")508 509 # check if type is valid510 if value in ALL_TYPES_SET:511 return value512 513 # translate from uppercase514 temp = value.lower()515 if temp in ALL_TYPES_SET:516 return temp517 518 # failure!519 raise ValueError("unknown argon2 hash type: %r" % (value,))520 521 @classmethod522 def _norm_version(cls, version):523 if not isinstance(version, uh.int_types):524 raise uh.exc.ExpectedTypeError(version, "integer", "version")525 526 # minimum valid version527 if version < 0x13 and version != 0x10:528 raise ValueError("invalid argon2 hash version: %d" % (version,))529 530 # check this isn't past backend's max version531 backend = cls.get_backend()532 if version > cls.max_version:533 raise ValueError("%s: hash version 0x%X not supported by %r backend "534 "(max version is 0x%X); try updating or switching backends" %535 (cls.name, version, backend, cls.max_version))536 return version537 538 @classmethod539 def _norm_memory_cost(cls, memory_cost, relaxed=False):540 return uh.norm_integer(cls, memory_cost, min=cls.min_memory_cost,541 param="memory_cost", relaxed=relaxed)542 543 #===================================================================544 # digest calculation545 #===================================================================546 547 # NOTE: _calc_checksum implemented by backend subclass548 549 @classmethod550 def _get_backend_type(cls, value):551 """552 helper to resolve backend constant from type553 """554 try:555 return cls._backend_type_map[value]556 except KeyError:557 pass558 # XXX: pick better error class?559 msg = "unsupported argon2 hash (type %r not supported by %s backend)" % \560 (value, cls.get_backend())561 raise ValueError(msg)562 563 #===================================================================564 # hash migration565 #===================================================================566 567 def _calc_needs_update(self, **kwds):568 cls = type(self)569 if self.type != cls.type:570 return True571 minver = cls.min_desired_version572 if minver is None or minver > cls.max_version:573 minver = cls.max_version574 if self.version < minver:575 # version is too old.576 return True577 if self.memory_cost != cls.memory_cost:578 return True579 if self.checksum_size != cls.checksum_size:580 return True581 return super(_Argon2Common, self)._calc_needs_update(**kwds)582 583 #===================================================================584 # backend loading585 #===================================================================586 587 _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install argon2_cffi')"588 589 @classmethod590 def _finalize_backend_mixin(mixin_cls, name, dryrun):591 """592 helper called by from backend mixin classes' _load_backend_mixin() --593 invoked after backend imports have been loaded, and performs594 feature detection & testing common to all backends.595 """596 # check argon2 version597 max_version = mixin_cls.max_version598 assert isinstance(max_version, int) and max_version >= 0x10599 if max_version < 0x13:600 warn("%r doesn't support argon2 v1.3, and should be upgraded" % name,601 uh.exc.PasslibSecurityWarning)602 603 # prefer best available type604 for type in ALL_TYPES:605 if type in mixin_cls._backend_type_map:606 mixin_cls.type = type607 break608 else:609 warn("%r lacks support for all known hash types" % name, uh.exc.PasslibRuntimeWarning)610 # NOTE: class will just throw "unsupported argon2 hash" error if they try to use it...611 mixin_cls.type = TYPE_ID612 613 return True614 615 @classmethod616 def _adapt_backend_error(cls, err, hash=None, self=None):617 """618 internal helper invoked when backend has hash/verification error;619 used to adapt to passlib message.620 """621 backend = cls.get_backend()622 623 # parse hash to throw error if format was invalid, parameter out of range, etc.624 if self is None and hash is not None:625 self = cls.from_string(hash)626 627 # check constraints on parsed object628 # XXX: could move this to __init__, but not needed by needs_update calls629 if self is not None:630 self._validate_constraints(self.memory_cost, self.parallelism)631 632 # as of cffi 16.1, lacks support in hash_secret(), so genhash() will get here.633 # as of cffi 16.2, support removed from verify_secret() as well.634 if backend == "argon2_cffi" and self.data is not None:635 raise NotImplementedError("argon2_cffi backend doesn't support the 'data' parameter")636 637 # fallback to reporting a malformed hash638 text = str(err)639 if text not in [640 "Decoding failed" # argon2_cffi's default message641 ]:642 reason = "%s reported: %s: hash=%r" % (backend, text, hash)643 else:644 reason = repr(hash)645 raise exc.MalformedHashError(cls, reason=reason)646 647 #===================================================================648 # eoc649 #===================================================================650 651#-----------------------------------------------------------------------652# stub backend653#-----------------------------------------------------------------------654class _NoBackend(_Argon2Common):655 """656 mixin used before any backend has been loaded.657 contains stubs that force loading of one of the available backends.658 """659 #===================================================================660 # primary methods661 #===================================================================662 @classmethod663 def hash(cls, secret):664 cls._stub_requires_backend()665 return cls.hash(secret)666 667 @classmethod668 def verify(cls, secret, hash):669 cls._stub_requires_backend()670 return cls.verify(secret, hash)671 672 @uh.deprecated_method(deprecated="1.7", removed="2.0")673 @classmethod674 def genhash(cls, secret, config):675 cls._stub_requires_backend()676 return cls.genhash(secret, config)677 678 #===================================================================679 # digest calculation680 #===================================================================681 def _calc_checksum(self, secret):682 # NOTE: since argon2_cffi takes care of rendering hash,683 # _calc_checksum() is only used by the argon2pure backend.684 self._stub_requires_backend()685 # NOTE: have to use super() here so that we don't recursively686 # call subclass's wrapped _calc_checksum687 return super(argon2, self)._calc_checksum(secret)688 689 #===================================================================690 # eoc691 #===================================================================692 693#-----------------------------------------------------------------------694# argon2_cffi backend695#-----------------------------------------------------------------------696class _CffiBackend(_Argon2Common):697 """698 argon2_cffi backend699 """700 #===================================================================701 # backend loading702 #===================================================================703 704 @classmethod705 def _load_backend_mixin(mixin_cls, name, dryrun):706 # make sure we write info to base class's __dict__, not that of a subclass707 assert mixin_cls is _CffiBackend708 709 # we automatically import this at top, so just grab info710 if _argon2_cffi is None:711 if _argon2_cffi_error:712 raise exc.PasslibSecurityError(_argon2_cffi_error)713 return False714 max_version = _argon2_cffi.low_level.ARGON2_VERSION715 log.debug("detected 'argon2_cffi' backend, version %r, with support for 0x%x argon2 hashes",716 _argon2_cffi.__version__, max_version)717 718 # build type map719 TypeEnum = _argon2_cffi.Type720 type_map = {}721 for type in ALL_TYPES:722 try:723 type_map[type] = getattr(TypeEnum, type.upper())724 except AttributeError:725 # TYPE_ID support not added until v18.2726 assert type not in (TYPE_I, TYPE_D), "unexpected missing type: %r" % type727 mixin_cls._backend_type_map = type_map728 729 # set version info, and run common setup730 mixin_cls.version = mixin_cls.max_version = max_version731 return mixin_cls._finalize_backend_mixin(name, dryrun)732 733 #===================================================================734 # primary methods735 #===================================================================736 @classmethod737 def hash(cls, secret):738 # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.739 uh.validate_secret(secret)740 secret = to_bytes(secret, "utf-8")741 # XXX: doesn't seem to be a way to make this honor max_threads742 try:743 return bascii_to_str(_argon2_cffi.low_level.hash_secret(744 type=cls._get_backend_type(cls.type),745 memory_cost=cls.memory_cost,746 time_cost=cls.default_rounds,747 parallelism=cls.parallelism,748 salt=to_bytes(cls._generate_salt()),749 hash_len=cls.checksum_size,750 secret=secret,751 ))752 except _argon2_cffi.exceptions.HashingError as err:753 raise cls._adapt_backend_error(err)754 755 #: helper for verify() method below -- maps prefixes to type constants756 _byte_ident_map = dict((render_bytes(b"$argon2%s$", type.encode("ascii")), type)757 for type in ALL_TYPES)758 759 @classmethod760 def verify(cls, secret, hash):761 # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.762 uh.validate_secret(secret)763 secret = to_bytes(secret, "utf-8")764 hash = to_bytes(hash, "ascii")765 766 # read type from start of hash767 # NOTE: don't care about malformed strings, lowlevel will throw error for us768 type = cls._byte_ident_map.get(hash[:1+hash.find(b"$", 1)], TYPE_I)769 type_code = cls._get_backend_type(type)770 771 # XXX: doesn't seem to be a way to make this honor max_threads772 try:773 result = _argon2_cffi.low_level.verify_secret(hash, secret, type_code)774 assert result is True775 return True776 except _argon2_cffi.exceptions.VerifyMismatchError:777 return False778 except _argon2_cffi.exceptions.VerificationError as err:779 raise cls._adapt_backend_error(err, hash=hash)780 781 # NOTE: deprecated, will be removed in 2.0782 @classmethod783 def genhash(cls, secret, config):784 # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.785 uh.validate_secret(secret)786 secret = to_bytes(secret, "utf-8")787 self = cls.from_string(config)788 # XXX: doesn't seem to be a way to make this honor max_threads789 try:790 result = bascii_to_str(_argon2_cffi.low_level.hash_secret(791 type=cls._get_backend_type(self.type),792 memory_cost=self.memory_cost,793 time_cost=self.rounds,794 parallelism=self.parallelism,795 salt=to_bytes(self.salt),796 hash_len=self.checksum_size,797 secret=secret,798 version=self.version,799 ))800 except _argon2_cffi.exceptions.HashingError as err:801 raise cls._adapt_backend_error(err, hash=config)802 if self.version == 0x10:803 # workaround: argon2 0x13 always returns "v=" segment, even for 0x10 hashes804 result = result.replace("$v=16$", "$")805 return result806 807 #===================================================================808 # digest calculation809 #===================================================================810 def _calc_checksum(self, secret):811 raise AssertionError("shouldn't be called under argon2_cffi backend")812 813 #===================================================================814 # eoc815 #===================================================================816 817#-----------------------------------------------------------------------818# argon2pure backend819#-----------------------------------------------------------------------820class _PureBackend(_Argon2Common):821 """822 argon2pure backend823 """824 #===================================================================825 # backend loading826 #===================================================================827 828 @classmethod829 def _load_backend_mixin(mixin_cls, name, dryrun):830 # make sure we write info to base class's __dict__, not that of a subclass831 assert mixin_cls is _PureBackend832 833 # import argon2pure834 global _argon2pure835 try:836 import argon2pure as _argon2pure837 except ImportError:838 return False839 840 # get default / max supported version -- added in v1.2.2841 try:842 from argon2pure import ARGON2_DEFAULT_VERSION as max_version843 except ImportError:844 log.warning("detected 'argon2pure' backend, but package is too old "845 "(passlib requires argon2pure >= 1.2.3)")846 return False847 848 log.debug("detected 'argon2pure' backend, with support for 0x%x argon2 hashes",849 max_version)850 851 if not dryrun:852 warn("Using argon2pure backend, which is 100x+ slower than is required "853 "for adequate security. Installing argon2_cffi (via 'pip install argon2_cffi') "854 "is strongly recommended", exc.PasslibSecurityWarning)855 856 # build type map857 type_map = {}858 for type in ALL_TYPES:859 try:860 type_map[type] = getattr(_argon2pure, "ARGON2" + type.upper())861 except AttributeError:862 # TYPE_ID support not added until v1.3863 assert type not in (TYPE_I, TYPE_D), "unexpected missing type: %r" % type864 mixin_cls._backend_type_map = type_map865 866 mixin_cls.version = mixin_cls.max_version = max_version867 return mixin_cls._finalize_backend_mixin(name, dryrun)868 869 #===================================================================870 # primary methods871 #===================================================================872 873 # NOTE: this backend uses default .hash() & .verify() implementations.874 875 #===================================================================876 # digest calculation877 #===================================================================878 def _calc_checksum(self, secret):879 # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.880 uh.validate_secret(secret)881 secret = to_bytes(secret, "utf-8")882 kwds = dict(883 password=secret,884 salt=self.salt,885 time_cost=self.rounds,886 memory_cost=self.memory_cost,887 parallelism=self.parallelism,888 tag_length=self.checksum_size,889 type_code=self._get_backend_type(self.type),890 version=self.version,891 )892 if self.max_threads > 0:893 kwds['threads'] = self.max_threads894 if self.pure_use_threads:895 kwds['use_threads'] = True896 if self.data:897 kwds['associated_data'] = self.data898 # NOTE: should return raw bytes899 # NOTE: this may raise _argon2pure.Argon2ParameterError,900 # but it if does that, there's a bug in our own parameter checking code.901 try:902 return _argon2pure.argon2(**kwds)903 except _argon2pure.Argon2Error as err:904 raise self._adapt_backend_error(err, self=self)905 906 #===================================================================907 # eoc908 #===================================================================909 910class argon2(_NoBackend, _Argon2Common):911 """912 This class implements the Argon2 password hash [#argon2-home]_, and follows the :ref:`password-hash-api`.913 914 Argon2 supports a variable-length salt, and variable time & memory cost,915 and a number of other configurable parameters.916 917 The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:918 919 :type type: str920 :param type:921 Specify the type of argon2 hash to generate.922 Can be one of "ID", "I", "D".923 924 This defaults to "ID" if supported by the backend, otherwise "I".925 926 :type salt: str927 :param salt:928 Optional salt string.929 If specified, the length must be between 0-1024 bytes.930 If not specified, one will be auto-generated (this is recommended).931 932 :type salt_size: int933 :param salt_size:934 Optional number of bytes to use when autogenerating new salts.935 936 :type rounds: int937 :param rounds:938 Optional number of rounds to use.939 This corresponds linearly to the amount of time hashing will take.940 941 :type time_cost: int942 :param time_cost:943 An alias for **rounds**, for compatibility with underlying argon2 library.944 945 :param int memory_cost:946 Defines the memory usage in kibibytes.947 This corresponds linearly to the amount of memory hashing will take.948 949 :param int parallelism:950 Defines the parallelization factor.951 *NOTE: this will affect the resulting hash value.*952 953 :param int digest_size:954 Length of the digest in bytes.955 956 :param int max_threads:957 Maximum number of threads that will be used.958 -1 means unlimited; otherwise hashing will use ``min(parallelism, max_threads)`` threads.959 960 .. note::961 962 This option is currently only honored by the argon2pure backend.963 964 :type relaxed: bool965 :param relaxed:966 By default, providing an invalid value for one of the other967 keywords will result in a :exc:`ValueError`. If ``relaxed=True``,968 and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`969 will be issued instead. Correctable errors include ``rounds``970 that are too small or too large, and ``salt`` strings that are too long.971 972 .. versionchanged:: 1.7.2973 974 Added the "type" keyword, and support for type "D" and "ID" hashes.975 (Prior versions could verify type "D" hashes, but not generate them).976 977 .. todo::978 979 * Support configurable threading limits.980 """981 #=============================================================================982 # backend983 #=============================================================================984 985 # NOTE: the brunt of the argon2 class is implemented in _Argon2Common.986 # there are then subclass for each backend (e.g. _PureBackend),987 # these are dynamically prepended to this class's bases988 # in order to load the appropriate backend.989 990 #: list of potential backends991 backends = ("argon2_cffi", "argon2pure")992 993 #: flag that this class's bases should be modified by SubclassBackendMixin994 _backend_mixin_target = True995 996 #: map of backend -> mixin class, used by _get_backend_loader()997 _backend_mixin_map = {998 None: _NoBackend,999 "argon2_cffi": _CffiBackend,1000 "argon2pure": _PureBackend,1001 }1002 1003 #=============================================================================1004 #1005 #=============================================================================1006 1007#=============================================================================1008# eof1009#=============================================================================1010 