Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 

1010 рядки
38 KiB

  1. """passlib.handlers.argon2 -- argon2 password hash wrapper
  2. References
  3. ==========
  4. * argon2
  5. - home: https://github.com/P-H-C/phc-winner-argon2
  6. - whitepaper: https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
  7. * argon2 cffi wrapper
  8. - pypi: https://pypi.python.org/pypi/argon2_cffi
  9. - home: https://github.com/hynek/argon2_cffi
  10. * argon2 pure python
  11. - pypi: https://pypi.python.org/pypi/argon2pure
  12. - home: https://github.com/bwesterb/argon2pure
  13. """
  14. #=============================================================================
  15. # imports
  16. #=============================================================================
  17. from __future__ import with_statement, absolute_import
  18. # core
  19. import logging
  20. log = logging.getLogger(__name__)
  21. import re
  22. import types
  23. from warnings import warn
  24. # site
  25. _argon2_cffi = None # loaded below
  26. _argon2pure = None # dynamically imported by _load_backend_argon2pure()
  27. # pkg
  28. from passlib import exc
  29. from passlib.crypto.digest import MAX_UINT32
  30. from passlib.utils import classproperty, to_bytes, render_bytes
  31. from passlib.utils.binary import b64s_encode, b64s_decode
  32. from passlib.utils.compat import u, unicode, bascii_to_str, uascii_to_str, PY2
  33. import passlib.utils.handlers as uh
  34. # local
  35. __all__ = [
  36. "argon2",
  37. ]
  38. #=============================================================================
  39. # helpers
  40. #=============================================================================
  41. # NOTE: when adding a new argon2 hash type, need to do the following:
  42. # * add TYPE_XXX constant, and add to ALL_TYPES
  43. # * make sure "_backend_type_map" constructors handle it correctly for all backends
  44. # * make sure _hash_regex & _ident_regex (below) support type string.
  45. # * add reference vectors for testing.
  46. #: argon2 type constants -- subclasses handle mapping these to backend-specific type constants.
  47. #: (should be lowercase, to match representation in hash string)
  48. TYPE_I = u("i")
  49. TYPE_D = u("d")
  50. TYPE_ID = u("id") # new 2016-10-29; passlib 1.7.2 requires backends new enough for support
  51. #: list of all known types; first (supported) type will be used as default.
  52. ALL_TYPES = (TYPE_ID, TYPE_I, TYPE_D)
  53. ALL_TYPES_SET = set(ALL_TYPES)
  54. #=============================================================================
  55. # import argon2 package (https://pypi.python.org/pypi/argon2_cffi)
  56. #=============================================================================
  57. # import cffi package
  58. # NOTE: we try to do this even if caller is going to use argon2pure,
  59. # so that we can always use the libargon2 default settings when possible.
  60. _argon2_cffi_error = None
  61. try:
  62. import argon2 as _argon2_cffi
  63. except ImportError:
  64. _argon2_cffi = None
  65. else:
  66. if not hasattr(_argon2_cffi, "Type"):
  67. # they have incompatible "argon2" package installed, instead of "argon2_cffi" package.
  68. _argon2_cffi_error = (
  69. "'argon2' module points to unsupported 'argon2' pypi package; "
  70. "please install 'argon2-cffi' instead."
  71. )
  72. _argon2_cffi = None
  73. elif not hasattr(_argon2_cffi, "low_level"):
  74. # they have pre-v16 argon2_cffi package
  75. _argon2_cffi_error = "'argon2-cffi' is too old, please update to argon2_cffi >= 18.2.0"
  76. _argon2_cffi = None
  77. # init default settings for our hasher class --
  78. # if we have argon2_cffi >= 16.0, use their default hasher settings, otherwise use static default
  79. if hasattr(_argon2_cffi, "PasswordHasher"):
  80. # use cffi's default settings
  81. _default_settings = _argon2_cffi.PasswordHasher()
  82. _default_version = _argon2_cffi.low_level.ARGON2_VERSION
  83. else:
  84. # use fallback settings (for no backend, or argon2pure)
  85. class _DummyCffiHasher:
  86. """
  87. dummy object to use as source of defaults when argon2_cffi isn't present.
  88. this tries to mimic the attributes of ``argon2.PasswordHasher()`` which the rest of
  89. this module reads.
  90. .. note:: values last synced w/ argon2 19.2 as of 2019-11-09
  91. """
  92. time_cost = 2
  93. memory_cost = 512
  94. parallelism = 2
  95. salt_len = 16
  96. hash_len = 16
  97. # NOTE: "type" attribute added in argon2_cffi v18.2; but currently not reading it
  98. # type = _argon2_cffi.Type.ID
  99. _default_settings = _DummyCffiHasher()
  100. _default_version = 0x13 # v1.9
  101. #=============================================================================
  102. # handler
  103. #=============================================================================
  104. class _Argon2Common(uh.SubclassBackendMixin, uh.ParallelismMixin,
  105. uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum,
  106. uh.GenericHandler):
  107. """
  108. Base class which implements brunt of Argon2 code.
  109. This is then subclassed by the various backends,
  110. to override w/ backend-specific methods.
  111. When a backend is loaded, the bases of the 'argon2' class proper
  112. are modified to prepend the correct backend-specific subclass.
  113. """
  114. #===================================================================
  115. # class attrs
  116. #===================================================================
  117. #------------------------
  118. # PasswordHash
  119. #------------------------
  120. name = "argon2"
  121. setting_kwds = ("salt",
  122. "salt_size",
  123. "salt_len", # 'salt_size' alias for compat w/ argon2 package
  124. "rounds",
  125. "time_cost", # 'rounds' alias for compat w/ argon2 package
  126. "memory_cost",
  127. "parallelism",
  128. "digest_size",
  129. "hash_len", # 'digest_size' alias for compat w/ argon2 package
  130. "type", # the type of argon2 hash used
  131. )
  132. # TODO: could support the optional 'data' parameter,
  133. # but need to research the uses, what a more descriptive name would be,
  134. # and deal w/ fact that argon2_cffi 16.1 doesn't currently support it.
  135. # (argon2_pure does though)
  136. #------------------------
  137. # GenericHandler
  138. #------------------------
  139. # NOTE: ident -- all argon2 hashes start with "$argon2<type>$"
  140. # XXX: could programmaticaly generate "ident_values" string from ALL_TYPES above
  141. checksum_size = _default_settings.hash_len
  142. #: force parsing these kwds
  143. _always_parse_settings = uh.GenericHandler._always_parse_settings + \
  144. ("type",)
  145. #: exclude these kwds from parsehash() result (most are aliases for other keys)
  146. _unparsed_settings = uh.GenericHandler._unparsed_settings + \
  147. ("salt_len", "time_cost", "hash_len", "digest_size")
  148. #------------------------
  149. # HasSalt
  150. #------------------------
  151. default_salt_size = _default_settings.salt_len
  152. min_salt_size = 8
  153. max_salt_size = MAX_UINT32
  154. #------------------------
  155. # HasRounds
  156. # TODO: once rounds limit logic is factored out,
  157. # make 'rounds' and 'cost' an alias for 'time_cost'
  158. #------------------------
  159. default_rounds = _default_settings.time_cost
  160. min_rounds = 1
  161. max_rounds = MAX_UINT32
  162. rounds_cost = "linear"
  163. #------------------------
  164. # ParalleismMixin
  165. #------------------------
  166. max_parallelism = (1 << 24) - 1 # from argon2.h / ARGON2_MAX_LANES
  167. #------------------------
  168. # custom
  169. #------------------------
  170. #: max version support
  171. #: NOTE: this is dependant on the backend, and initialized/modified by set_backend()
  172. max_version = _default_version
  173. #: minimum version before needs_update() marks the hash; if None, defaults to max_version
  174. min_desired_version = None
  175. #: minimum valid memory_cost
  176. min_memory_cost = 8 # from argon2.h / ARGON2_MIN_MEMORY
  177. #: maximum number of threads (-1=unlimited);
  178. #: number of threads used by .hash() will be min(parallelism, max_threads)
  179. max_threads = -1
  180. #: global flag signalling argon2pure backend to use threads
  181. #: rather than subprocesses.
  182. pure_use_threads = False
  183. #: internal helper used to store mapping of TYPE_XXX constants -> backend-specific type constants;
  184. #: this is populated by _load_backend_mixin(); and used to detect which types are supported.
  185. #: XXX: could expose keys as class-level .supported_types property?
  186. _backend_type_map = {}
  187. @classproperty
  188. def type_values(cls):
  189. """
  190. return tuple of types supported by this backend
  191. .. versionadded:: 1.7.2
  192. """
  193. cls.get_backend() # make sure backend is loaded
  194. return tuple(cls._backend_type_map)
  195. #===================================================================
  196. # instance attrs
  197. #===================================================================
  198. #: argon2 hash type, one of ALL_TYPES -- class value controls the default
  199. #: .. versionadded:: 1.7.2
  200. type = TYPE_ID
  201. #: parallelism setting -- class value controls the default
  202. parallelism = _default_settings.parallelism
  203. #: hash version (int)
  204. #: NOTE: this is modified by set_backend()
  205. version = _default_version
  206. #: memory cost -- class value controls the default
  207. memory_cost = _default_settings.memory_cost
  208. @property
  209. def type_d(self):
  210. """
  211. flag indicating a Type D hash
  212. .. deprecated:: 1.7.2; will be removed in passlib 2.0
  213. """
  214. return self.type == TYPE_D
  215. #: optional secret data
  216. data = None
  217. #===================================================================
  218. # variant constructor
  219. #===================================================================
  220. @classmethod
  221. def using(cls, type=None, memory_cost=None, salt_len=None, time_cost=None, digest_size=None,
  222. checksum_size=None, hash_len=None, max_threads=None, **kwds):
  223. # support aliases which match argon2 naming convention
  224. if time_cost is not None:
  225. if "rounds" in kwds:
  226. raise TypeError("'time_cost' and 'rounds' are mutually exclusive")
  227. kwds['rounds'] = time_cost
  228. if salt_len is not None:
  229. if "salt_size" in kwds:
  230. raise TypeError("'salt_len' and 'salt_size' are mutually exclusive")
  231. kwds['salt_size'] = salt_len
  232. if hash_len is not None:
  233. if digest_size is not None:
  234. raise TypeError("'hash_len' and 'digest_size' are mutually exclusive")
  235. digest_size = hash_len
  236. if checksum_size is not None:
  237. if digest_size is not None:
  238. raise TypeError("'checksum_size' and 'digest_size' are mutually exclusive")
  239. digest_size = checksum_size
  240. # create variant
  241. subcls = super(_Argon2Common, cls).using(**kwds)
  242. # set type
  243. if type is not None:
  244. subcls.type = subcls._norm_type(type)
  245. # set checksum size
  246. relaxed = kwds.get("relaxed")
  247. if digest_size is not None:
  248. if isinstance(digest_size, uh.native_string_types):
  249. digest_size = int(digest_size)
  250. # NOTE: this isn't *really* digest size minimum, but want to enforce secure minimum.
  251. subcls.checksum_size = uh.norm_integer(subcls, digest_size, min=16, max=MAX_UINT32,
  252. param="digest_size", relaxed=relaxed)
  253. # set memory cost
  254. if memory_cost is not None:
  255. if isinstance(memory_cost, uh.native_string_types):
  256. memory_cost = int(memory_cost)
  257. subcls.memory_cost = subcls._norm_memory_cost(memory_cost, relaxed=relaxed)
  258. # validate constraints
  259. subcls._validate_constraints(subcls.memory_cost, subcls.parallelism)
  260. # set max threads
  261. if max_threads is not None:
  262. if isinstance(max_threads, uh.native_string_types):
  263. max_threads = int(max_threads)
  264. if max_threads < 1 and max_threads != -1:
  265. raise ValueError("max_threads (%d) must be -1 (unlimited), or at least 1." %
  266. (max_threads,))
  267. subcls.max_threads = max_threads
  268. return subcls
  269. @classmethod
  270. def _validate_constraints(cls, memory_cost, parallelism):
  271. # NOTE: this is used by class & instance, hence passing in via arguments.
  272. # could switch and make this a hybrid method.
  273. min_memory_cost = 8 * parallelism
  274. if memory_cost < min_memory_cost:
  275. raise ValueError("%s: memory_cost (%d) is too low, must be at least "
  276. "8 * parallelism (8 * %d = %d)" %
  277. (cls.name, memory_cost,
  278. parallelism, min_memory_cost))
  279. #===================================================================
  280. # public api
  281. #===================================================================
  282. #: shorter version of _hash_regex, used to quickly identify hashes
  283. _ident_regex = re.compile(r"^\$argon2[a-z]+\$")
  284. @classmethod
  285. def identify(cls, hash):
  286. hash = uh.to_unicode_for_identify(hash)
  287. return cls._ident_regex.match(hash) is not None
  288. # hash(), verify(), genhash() -- implemented by backend subclass
  289. #===================================================================
  290. # hash parsing / rendering
  291. #===================================================================
  292. # info taken from source of decode_string() function in
  293. # <https://github.com/P-H-C/phc-winner-argon2/blob/master/src/encoding.c>
  294. #
  295. # hash format:
  296. # $argon2<T>[$v=<num>]$m=<num>,t=<num>,p=<num>[,keyid=<bin>][,data=<bin>][$<bin>[$<bin>]]
  297. #
  298. # NOTE: as of 2016-6-17, the official source (above) lists the "keyid" param in the comments,
  299. # but the actual source of decode_string & encode_string don't mention it at all.
  300. # we're supporting parsing it, but throw NotImplementedError if encountered.
  301. #
  302. # sample hashes:
  303. # v1.0: '$argon2i$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'
  304. # v1.3: '$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'
  305. #: regex to parse argon hash
  306. _hash_regex = re.compile(br"""
  307. ^
  308. \$argon2(?P<type>[a-z]+)\$
  309. (?:
  310. v=(?P<version>\d+)
  311. \$
  312. )?
  313. m=(?P<memory_cost>\d+)
  314. ,
  315. t=(?P<time_cost>\d+)
  316. ,
  317. p=(?P<parallelism>\d+)
  318. (?:
  319. ,keyid=(?P<keyid>[^,$]+)
  320. )?
  321. (?:
  322. ,data=(?P<data>[^,$]+)
  323. )?
  324. (?:
  325. \$
  326. (?P<salt>[^$]+)
  327. (?:
  328. \$
  329. (?P<digest>.+)
  330. )?
  331. )?
  332. $
  333. """, re.X)
  334. @classmethod
  335. def from_string(cls, hash):
  336. # NOTE: assuming hash will be unicode, or use ascii-compatible encoding.
  337. # TODO: switch to working w/ str or unicode
  338. if isinstance(hash, unicode):
  339. hash = hash.encode("utf-8")
  340. if not isinstance(hash, bytes):
  341. raise exc.ExpectedStringError(hash, "hash")
  342. m = cls._hash_regex.match(hash)
  343. if not m:
  344. raise exc.MalformedHashError(cls)
  345. type, version, memory_cost, time_cost, parallelism, keyid, data, salt, digest = \
  346. m.group("type", "version", "memory_cost", "time_cost", "parallelism",
  347. "keyid", "data", "salt", "digest")
  348. if keyid:
  349. raise NotImplementedError("argon2 'keyid' parameter not supported")
  350. return cls(
  351. type=type.decode("ascii"),
  352. version=int(version) if version else 0x10,
  353. memory_cost=int(memory_cost),
  354. rounds=int(time_cost),
  355. parallelism=int(parallelism),
  356. salt=b64s_decode(salt) if salt else None,
  357. data=b64s_decode(data) if data else None,
  358. checksum=b64s_decode(digest) if digest else None,
  359. )
  360. def to_string(self):
  361. version = self.version
  362. if version == 0x10:
  363. vstr = ""
  364. else:
  365. vstr = "v=%d$" % version
  366. data = self.data
  367. if data:
  368. kdstr = ",data=" + bascii_to_str(b64s_encode(self.data))
  369. else:
  370. kdstr = ""
  371. # NOTE: 'keyid' param currently not supported
  372. return "$argon2%s$%sm=%d,t=%d,p=%d%s$%s$%s" % (
  373. uascii_to_str(self.type),
  374. vstr,
  375. self.memory_cost,
  376. self.rounds,
  377. self.parallelism,
  378. kdstr,
  379. bascii_to_str(b64s_encode(self.salt)),
  380. bascii_to_str(b64s_encode(self.checksum)),
  381. )
  382. #===================================================================
  383. # init
  384. #===================================================================
  385. def __init__(self, type=None, type_d=False, version=None, memory_cost=None, data=None, **kwds):
  386. # handle deprecated kwds
  387. if type_d:
  388. warn('argon2 `type_d=True` keyword is deprecated, and will be removed in passlib 2.0; '
  389. 'please use ``type="d"`` instead')
  390. assert type is None
  391. type = TYPE_D
  392. # TODO: factor out variable checksum size support into a mixin.
  393. # set checksum size to specific value before _norm_checksum() is called
  394. checksum = kwds.get("checksum")
  395. if checksum is not None:
  396. self.checksum_size = len(checksum)
  397. # call parent
  398. super(_Argon2Common, self).__init__(**kwds)
  399. # init type
  400. if type is None:
  401. assert uh.validate_default_value(self, self.type, self._norm_type, param="type")
  402. else:
  403. self.type = self._norm_type(type)
  404. # init version
  405. if version is None:
  406. assert uh.validate_default_value(self, self.version, self._norm_version,
  407. param="version")
  408. else:
  409. self.version = self._norm_version(version)
  410. # init memory cost
  411. if memory_cost is None:
  412. assert uh.validate_default_value(self, self.memory_cost, self._norm_memory_cost,
  413. param="memory_cost")
  414. else:
  415. self.memory_cost = self._norm_memory_cost(memory_cost)
  416. # init data
  417. if data is None:
  418. assert self.data is None
  419. else:
  420. if not isinstance(data, bytes):
  421. raise uh.exc.ExpectedTypeError(data, "bytes", "data")
  422. self.data = data
  423. #-------------------------------------------------------------------
  424. # parameter guards
  425. #-------------------------------------------------------------------
  426. @classmethod
  427. def _norm_type(cls, value):
  428. # type check
  429. if not isinstance(value, unicode):
  430. if PY2 and isinstance(value, bytes):
  431. value = value.decode('ascii')
  432. else:
  433. raise uh.exc.ExpectedTypeError(value, "str", "type")
  434. # check if type is valid
  435. if value in ALL_TYPES_SET:
  436. return value
  437. # translate from uppercase
  438. temp = value.lower()
  439. if temp in ALL_TYPES_SET:
  440. return temp
  441. # failure!
  442. raise ValueError("unknown argon2 hash type: %r" % (value,))
  443. @classmethod
  444. def _norm_version(cls, version):
  445. if not isinstance(version, uh.int_types):
  446. raise uh.exc.ExpectedTypeError(version, "integer", "version")
  447. # minimum valid version
  448. if version < 0x13 and version != 0x10:
  449. raise ValueError("invalid argon2 hash version: %d" % (version,))
  450. # check this isn't past backend's max version
  451. backend = cls.get_backend()
  452. if version > cls.max_version:
  453. raise ValueError("%s: hash version 0x%X not supported by %r backend "
  454. "(max version is 0x%X); try updating or switching backends" %
  455. (cls.name, version, backend, cls.max_version))
  456. return version
  457. @classmethod
  458. def _norm_memory_cost(cls, memory_cost, relaxed=False):
  459. return uh.norm_integer(cls, memory_cost, min=cls.min_memory_cost,
  460. param="memory_cost", relaxed=relaxed)
  461. #===================================================================
  462. # digest calculation
  463. #===================================================================
  464. # NOTE: _calc_checksum implemented by backend subclass
  465. @classmethod
  466. def _get_backend_type(cls, value):
  467. """
  468. helper to resolve backend constant from type
  469. """
  470. try:
  471. return cls._backend_type_map[value]
  472. except KeyError:
  473. pass
  474. # XXX: pick better error class?
  475. msg = "unsupported argon2 hash (type %r not supported by %s backend)" % \
  476. (value, cls.get_backend())
  477. raise ValueError(msg)
  478. #===================================================================
  479. # hash migration
  480. #===================================================================
  481. def _calc_needs_update(self, **kwds):
  482. cls = type(self)
  483. if self.type != cls.type:
  484. return True
  485. minver = cls.min_desired_version
  486. if minver is None or minver > cls.max_version:
  487. minver = cls.max_version
  488. if self.version < minver:
  489. # version is too old.
  490. return True
  491. if self.memory_cost != cls.memory_cost:
  492. return True
  493. if self.checksum_size != cls.checksum_size:
  494. return True
  495. return super(_Argon2Common, self)._calc_needs_update(**kwds)
  496. #===================================================================
  497. # backend loading
  498. #===================================================================
  499. _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install argon2_cffi')"
  500. @classmethod
  501. def _finalize_backend_mixin(mixin_cls, name, dryrun):
  502. """
  503. helper called by from backend mixin classes' _load_backend_mixin() --
  504. invoked after backend imports have been loaded, and performs
  505. feature detection & testing common to all backends.
  506. """
  507. # check argon2 version
  508. max_version = mixin_cls.max_version
  509. assert isinstance(max_version, int) and max_version >= 0x10
  510. if max_version < 0x13:
  511. warn("%r doesn't support argon2 v1.3, and should be upgraded" % name,
  512. uh.exc.PasslibSecurityWarning)
  513. # prefer best available type
  514. for type in ALL_TYPES:
  515. if type in mixin_cls._backend_type_map:
  516. mixin_cls.type = type
  517. break
  518. else:
  519. warn("%r lacks support for all known hash types" % name, uh.exc.PasslibRuntimeWarning)
  520. # NOTE: class will just throw "unsupported argon2 hash" error if they try to use it...
  521. mixin_cls.type = TYPE_ID
  522. return True
  523. @classmethod
  524. def _adapt_backend_error(cls, err, hash=None, self=None):
  525. """
  526. internal helper invoked when backend has hash/verification error;
  527. used to adapt to passlib message.
  528. """
  529. backend = cls.get_backend()
  530. # parse hash to throw error if format was invalid, parameter out of range, etc.
  531. if self is None and hash is not None:
  532. self = cls.from_string(hash)
  533. # check constraints on parsed object
  534. # XXX: could move this to __init__, but not needed by needs_update calls
  535. if self is not None:
  536. self._validate_constraints(self.memory_cost, self.parallelism)
  537. # as of cffi 16.1, lacks support in hash_secret(), so genhash() will get here.
  538. # as of cffi 16.2, support removed from verify_secret() as well.
  539. if backend == "argon2_cffi" and self.data is not None:
  540. raise NotImplementedError("argon2_cffi backend doesn't support the 'data' parameter")
  541. # fallback to reporting a malformed hash
  542. text = str(err)
  543. if text not in [
  544. "Decoding failed" # argon2_cffi's default message
  545. ]:
  546. reason = "%s reported: %s: hash=%r" % (backend, text, hash)
  547. else:
  548. reason = repr(hash)
  549. raise exc.MalformedHashError(cls, reason=reason)
  550. #===================================================================
  551. # eoc
  552. #===================================================================
  553. #-----------------------------------------------------------------------
  554. # stub backend
  555. #-----------------------------------------------------------------------
  556. class _NoBackend(_Argon2Common):
  557. """
  558. mixin used before any backend has been loaded.
  559. contains stubs that force loading of one of the available backends.
  560. """
  561. #===================================================================
  562. # primary methods
  563. #===================================================================
  564. @classmethod
  565. def hash(cls, secret):
  566. cls._stub_requires_backend()
  567. return cls.hash(secret)
  568. @classmethod
  569. def verify(cls, secret, hash):
  570. cls._stub_requires_backend()
  571. return cls.verify(secret, hash)
  572. @uh.deprecated_method(deprecated="1.7", removed="2.0")
  573. @classmethod
  574. def genhash(cls, secret, config):
  575. cls._stub_requires_backend()
  576. return cls.genhash(secret, config)
  577. #===================================================================
  578. # digest calculation
  579. #===================================================================
  580. def _calc_checksum(self, secret):
  581. # NOTE: since argon2_cffi takes care of rendering hash,
  582. # _calc_checksum() is only used by the argon2pure backend.
  583. self._stub_requires_backend()
  584. # NOTE: have to use super() here so that we don't recursively
  585. # call subclass's wrapped _calc_checksum
  586. return super(argon2, self)._calc_checksum(secret)
  587. #===================================================================
  588. # eoc
  589. #===================================================================
  590. #-----------------------------------------------------------------------
  591. # argon2_cffi backend
  592. #-----------------------------------------------------------------------
  593. class _CffiBackend(_Argon2Common):
  594. """
  595. argon2_cffi backend
  596. """
  597. #===================================================================
  598. # backend loading
  599. #===================================================================
  600. @classmethod
  601. def _load_backend_mixin(mixin_cls, name, dryrun):
  602. # make sure we write info to base class's __dict__, not that of a subclass
  603. assert mixin_cls is _CffiBackend
  604. # we automatically import this at top, so just grab info
  605. if _argon2_cffi is None:
  606. if _argon2_cffi_error:
  607. raise exc.PasslibSecurityError(_argon2_cffi_error)
  608. return False
  609. max_version = _argon2_cffi.low_level.ARGON2_VERSION
  610. log.debug("detected 'argon2_cffi' backend, version %r, with support for 0x%x argon2 hashes",
  611. _argon2_cffi.__version__, max_version)
  612. # build type map
  613. TypeEnum = _argon2_cffi.Type
  614. type_map = {}
  615. for type in ALL_TYPES:
  616. try:
  617. type_map[type] = getattr(TypeEnum, type.upper())
  618. except AttributeError:
  619. # TYPE_ID support not added until v18.2
  620. assert type not in (TYPE_I, TYPE_D), "unexpected missing type: %r" % type
  621. mixin_cls._backend_type_map = type_map
  622. # set version info, and run common setup
  623. mixin_cls.version = mixin_cls.max_version = max_version
  624. return mixin_cls._finalize_backend_mixin(name, dryrun)
  625. #===================================================================
  626. # primary methods
  627. #===================================================================
  628. @classmethod
  629. def hash(cls, secret):
  630. # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.
  631. uh.validate_secret(secret)
  632. secret = to_bytes(secret, "utf-8")
  633. # XXX: doesn't seem to be a way to make this honor max_threads
  634. try:
  635. return bascii_to_str(_argon2_cffi.low_level.hash_secret(
  636. type=cls._get_backend_type(cls.type),
  637. memory_cost=cls.memory_cost,
  638. time_cost=cls.default_rounds,
  639. parallelism=cls.parallelism,
  640. salt=to_bytes(cls._generate_salt()),
  641. hash_len=cls.checksum_size,
  642. secret=secret,
  643. ))
  644. except _argon2_cffi.exceptions.HashingError as err:
  645. raise cls._adapt_backend_error(err)
  646. #: helper for verify() method below -- maps prefixes to type constants
  647. _byte_ident_map = dict((render_bytes(b"$argon2%s$", type.encode("ascii")), type)
  648. for type in ALL_TYPES)
  649. @classmethod
  650. def verify(cls, secret, hash):
  651. # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.
  652. uh.validate_secret(secret)
  653. secret = to_bytes(secret, "utf-8")
  654. hash = to_bytes(hash, "ascii")
  655. # read type from start of hash
  656. # NOTE: don't care about malformed strings, lowlevel will throw error for us
  657. type = cls._byte_ident_map.get(hash[:1+hash.find(b"$", 1)], TYPE_I)
  658. type_code = cls._get_backend_type(type)
  659. # XXX: doesn't seem to be a way to make this honor max_threads
  660. try:
  661. result = _argon2_cffi.low_level.verify_secret(hash, secret, type_code)
  662. assert result is True
  663. return True
  664. except _argon2_cffi.exceptions.VerifyMismatchError:
  665. return False
  666. except _argon2_cffi.exceptions.VerificationError as err:
  667. raise cls._adapt_backend_error(err, hash=hash)
  668. # NOTE: deprecated, will be removed in 2.0
  669. @classmethod
  670. def genhash(cls, secret, config):
  671. # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.
  672. uh.validate_secret(secret)
  673. secret = to_bytes(secret, "utf-8")
  674. self = cls.from_string(config)
  675. # XXX: doesn't seem to be a way to make this honor max_threads
  676. try:
  677. result = bascii_to_str(_argon2_cffi.low_level.hash_secret(
  678. type=cls._get_backend_type(self.type),
  679. memory_cost=self.memory_cost,
  680. time_cost=self.rounds,
  681. parallelism=self.parallelism,
  682. salt=to_bytes(self.salt),
  683. hash_len=self.checksum_size,
  684. secret=secret,
  685. version=self.version,
  686. ))
  687. except _argon2_cffi.exceptions.HashingError as err:
  688. raise cls._adapt_backend_error(err, hash=config)
  689. if self.version == 0x10:
  690. # workaround: argon2 0x13 always returns "v=" segment, even for 0x10 hashes
  691. result = result.replace("$v=16$", "$")
  692. return result
  693. #===================================================================
  694. # digest calculation
  695. #===================================================================
  696. def _calc_checksum(self, secret):
  697. raise AssertionError("shouldn't be called under argon2_cffi backend")
  698. #===================================================================
  699. # eoc
  700. #===================================================================
  701. #-----------------------------------------------------------------------
  702. # argon2pure backend
  703. #-----------------------------------------------------------------------
  704. class _PureBackend(_Argon2Common):
  705. """
  706. argon2pure backend
  707. """
  708. #===================================================================
  709. # backend loading
  710. #===================================================================
  711. @classmethod
  712. def _load_backend_mixin(mixin_cls, name, dryrun):
  713. # make sure we write info to base class's __dict__, not that of a subclass
  714. assert mixin_cls is _PureBackend
  715. # import argon2pure
  716. global _argon2pure
  717. try:
  718. import argon2pure as _argon2pure
  719. except ImportError:
  720. return False
  721. # get default / max supported version -- added in v1.2.2
  722. try:
  723. from argon2pure import ARGON2_DEFAULT_VERSION as max_version
  724. except ImportError:
  725. log.warning("detected 'argon2pure' backend, but package is too old "
  726. "(passlib requires argon2pure >= 1.2.3)")
  727. return False
  728. log.debug("detected 'argon2pure' backend, with support for 0x%x argon2 hashes",
  729. max_version)
  730. if not dryrun:
  731. warn("Using argon2pure backend, which is 100x+ slower than is required "
  732. "for adequate security. Installing argon2_cffi (via 'pip install argon2_cffi') "
  733. "is strongly recommended", exc.PasslibSecurityWarning)
  734. # build type map
  735. type_map = {}
  736. for type in ALL_TYPES:
  737. try:
  738. type_map[type] = getattr(_argon2pure, "ARGON2" + type.upper())
  739. except AttributeError:
  740. # TYPE_ID support not added until v1.3
  741. assert type not in (TYPE_I, TYPE_D), "unexpected missing type: %r" % type
  742. mixin_cls._backend_type_map = type_map
  743. mixin_cls.version = mixin_cls.max_version = max_version
  744. return mixin_cls._finalize_backend_mixin(name, dryrun)
  745. #===================================================================
  746. # primary methods
  747. #===================================================================
  748. # NOTE: this backend uses default .hash() & .verify() implementations.
  749. #===================================================================
  750. # digest calculation
  751. #===================================================================
  752. def _calc_checksum(self, secret):
  753. # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.
  754. uh.validate_secret(secret)
  755. secret = to_bytes(secret, "utf-8")
  756. kwds = dict(
  757. password=secret,
  758. salt=self.salt,
  759. time_cost=self.rounds,
  760. memory_cost=self.memory_cost,
  761. parallelism=self.parallelism,
  762. tag_length=self.checksum_size,
  763. type_code=self._get_backend_type(self.type),
  764. version=self.version,
  765. )
  766. if self.max_threads > 0:
  767. kwds['threads'] = self.max_threads
  768. if self.pure_use_threads:
  769. kwds['use_threads'] = True
  770. if self.data:
  771. kwds['associated_data'] = self.data
  772. # NOTE: should return raw bytes
  773. # NOTE: this may raise _argon2pure.Argon2ParameterError,
  774. # but it if does that, there's a bug in our own parameter checking code.
  775. try:
  776. return _argon2pure.argon2(**kwds)
  777. except _argon2pure.Argon2Error as err:
  778. raise self._adapt_backend_error(err, self=self)
  779. #===================================================================
  780. # eoc
  781. #===================================================================
  782. class argon2(_NoBackend, _Argon2Common):
  783. """
  784. This class implements the Argon2 password hash [#argon2-home]_, and follows the :ref:`password-hash-api`.
  785. Argon2 supports a variable-length salt, and variable time & memory cost,
  786. and a number of other configurable parameters.
  787. The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
  788. :type type: str
  789. :param type:
  790. Specify the type of argon2 hash to generate.
  791. Can be one of "ID", "I", "D".
  792. This defaults to "ID" if supported by the backend, otherwise "I".
  793. :type salt: str
  794. :param salt:
  795. Optional salt string.
  796. If specified, the length must be between 0-1024 bytes.
  797. If not specified, one will be auto-generated (this is recommended).
  798. :type salt_size: int
  799. :param salt_size:
  800. Optional number of bytes to use when autogenerating new salts.
  801. :type rounds: int
  802. :param rounds:
  803. Optional number of rounds to use.
  804. This corresponds linearly to the amount of time hashing will take.
  805. :type time_cost: int
  806. :param time_cost:
  807. An alias for **rounds**, for compatibility with underlying argon2 library.
  808. :param int memory_cost:
  809. Defines the memory usage in kibibytes.
  810. This corresponds linearly to the amount of memory hashing will take.
  811. :param int parallelism:
  812. Defines the parallelization factor.
  813. *NOTE: this will affect the resulting hash value.*
  814. :param int digest_size:
  815. Length of the digest in bytes.
  816. :param int max_threads:
  817. Maximum number of threads that will be used.
  818. -1 means unlimited; otherwise hashing will use ``min(parallelism, max_threads)`` threads.
  819. .. note::
  820. This option is currently only honored by the argon2pure backend.
  821. :type relaxed: bool
  822. :param relaxed:
  823. By default, providing an invalid value for one of the other
  824. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  825. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  826. will be issued instead. Correctable errors include ``rounds``
  827. that are too small or too large, and ``salt`` strings that are too long.
  828. .. versionchanged:: 1.7.2
  829. Added the "type" keyword, and support for type "D" and "ID" hashes.
  830. (Prior versions could verify type "D" hashes, but not generate them).
  831. .. todo::
  832. * Support configurable threading limits.
  833. """
  834. #=============================================================================
  835. # backend
  836. #=============================================================================
  837. # NOTE: the brunt of the argon2 class is implemented in _Argon2Common.
  838. # there are then subclass for each backend (e.g. _PureBackend),
  839. # these are dynamically prepended to this class's bases
  840. # in order to load the appropriate backend.
  841. #: list of potential backends
  842. backends = ("argon2_cffi", "argon2pure")
  843. #: flag that this class's bases should be modified by SubclassBackendMixin
  844. _backend_mixin_target = True
  845. #: map of backend -> mixin class, used by _get_backend_loader()
  846. _backend_mixin_map = {
  847. None: _NoBackend,
  848. "argon2_cffi": _CffiBackend,
  849. "argon2pure": _PureBackend,
  850. }
  851. #=============================================================================
  852. #
  853. #=============================================================================
  854. #=============================================================================
  855. # eof
  856. #=============================================================================