You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

826 lines
31 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 to_bytes
  31. from passlib.utils.binary import b64s_encode, b64s_decode
  32. from passlib.utils.compat import u, unicode, bascii_to_str
  33. import passlib.utils.handlers as uh
  34. # local
  35. __all__ = [
  36. "argon2",
  37. ]
  38. #=============================================================================
  39. # import argon2 package (https://pypi.python.org/pypi/argon2_cffi)
  40. #=============================================================================
  41. # import package
  42. try:
  43. import argon2 as _argon2_cffi
  44. except ImportError:
  45. _argon2_cffi = None
  46. # get default settings for hasher
  47. _PasswordHasher = getattr(_argon2_cffi, "PasswordHasher", None)
  48. if _PasswordHasher:
  49. # we have argon2_cffi >= 16.0, use their default hasher settings
  50. _default_settings = _PasswordHasher()
  51. _default_version = _argon2_cffi.low_level.ARGON2_VERSION
  52. else:
  53. # use these as our fallback settings (for no backend, or argon2pure)
  54. class _default_settings:
  55. """
  56. dummy object to use as source of defaults when argon2 mod not present.
  57. synced w/ argon2 16.1 as of 2016-6-16
  58. """
  59. time_cost = 2
  60. memory_cost = 512
  61. parallelism = 2
  62. salt_len = 16
  63. hash_len = 16
  64. _default_version = 0x13
  65. #=============================================================================
  66. # handler
  67. #=============================================================================
  68. class _Argon2Common(uh.SubclassBackendMixin, uh.ParallelismMixin,
  69. uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum,
  70. uh.GenericHandler):
  71. """
  72. Base class which implements brunt of Argon2 code.
  73. This is then subclassed by the various backends,
  74. to override w/ backend-specific methods.
  75. When a backend is loaded, the bases of the 'argon2' class proper
  76. are modified to prepend the correct backend-specific subclass.
  77. """
  78. #===================================================================
  79. # class attrs
  80. #===================================================================
  81. #------------------------
  82. # PasswordHash
  83. #------------------------
  84. name = "argon2"
  85. setting_kwds = ("salt",
  86. "salt_size",
  87. "salt_len", # 'salt_size' alias for compat w/ argon2 package
  88. "rounds",
  89. "time_cost", # 'rounds' alias for compat w/ argon2 package
  90. "memory_cost",
  91. "parallelism",
  92. "digest_size",
  93. "hash_len", # 'digest_size' alias for compat w/ argon2 package
  94. )
  95. # TODO: could support the optional 'data' parameter,
  96. # but need to research the uses, what a more descriptive name would be,
  97. # and deal w/ fact that argon2_cffi 16.1 doesn't currently support it.
  98. # (argon2_pure does though)
  99. #------------------------
  100. # GenericHandler
  101. #------------------------
  102. ident = u("$argon2i")
  103. checksum_size = _default_settings.hash_len
  104. # NOTE: from_string() relies on the ordering of these...
  105. ident_values = (u("$argon2i$"), u("$argon2d$"))
  106. #------------------------
  107. # HasSalt
  108. #------------------------
  109. default_salt_size = _default_settings.salt_len
  110. min_salt_size = 8
  111. max_salt_size = MAX_UINT32
  112. #------------------------
  113. # HasRounds
  114. # TODO: once rounds limit logic is factored out,
  115. # make 'rounds' and 'cost' an alias for 'time_cost'
  116. #------------------------
  117. default_rounds = _default_settings.time_cost
  118. min_rounds = 1
  119. max_rounds = MAX_UINT32
  120. rounds_cost = "linear"
  121. #------------------------
  122. # ParalleismMixin
  123. #------------------------
  124. max_parallelism = (1 << 24) - 1 # from argon2.h / ARGON2_MAX_LANES
  125. #------------------------
  126. # custom
  127. #------------------------
  128. #: max version support
  129. #: NOTE: this is dependant on the backend, and initialized/modified by set_backend()
  130. max_version = _default_version
  131. #: minimum version before needs_update() marks the hash; if None, defaults to max_version
  132. min_desired_version = None
  133. #: minimum valid memory_cost
  134. min_memory_cost = 8 # from argon2.h / ARGON2_MIN_MEMORY
  135. #: maximum number of threads (-1=unlimited);
  136. #: number of threads used by .hash() will be min(parallelism, max_threads)
  137. max_threads = -1
  138. #: global flag signalling argon2pure backend to use threads
  139. #: rather than subprocesses.
  140. pure_use_threads = False
  141. #===================================================================
  142. # instance attrs
  143. #===================================================================
  144. #: parallelism setting -- class value controls the default
  145. parallelism = _default_settings.parallelism
  146. #: hash version (int)
  147. #: NOTE: this is modified by set_backend()
  148. version = _default_version
  149. #: memory cost -- class value controls the default
  150. memory_cost = _default_settings.memory_cost
  151. #: flag indicating a Type D hash
  152. type_d = False
  153. #: optional secret data
  154. data = None
  155. #===================================================================
  156. # variant constructor
  157. #===================================================================
  158. @classmethod
  159. def using(cls, memory_cost=None, salt_len=None, time_cost=None, digest_size=None,
  160. checksum_size=None, hash_len=None, max_threads=None, **kwds):
  161. # support aliases which match argon2 naming convention
  162. if time_cost is not None:
  163. if "rounds" in kwds:
  164. raise TypeError("'time_cost' and 'rounds' are mutually exclusive")
  165. kwds['rounds'] = time_cost
  166. if salt_len is not None:
  167. if "salt_size" in kwds:
  168. raise TypeError("'salt_len' and 'salt_size' are mutually exclusive")
  169. kwds['salt_size'] = salt_len
  170. if hash_len is not None:
  171. if digest_size is not None:
  172. raise TypeError("'hash_len' and 'digest_size' are mutually exclusive")
  173. digest_size = hash_len
  174. if checksum_size is not None:
  175. if digest_size is not None:
  176. raise TypeError("'checksum_size' and 'digest_size' are mutually exclusive")
  177. digest_size = checksum_size
  178. # create variant
  179. subcls = super(_Argon2Common, cls).using(**kwds)
  180. # set checksum size
  181. relaxed = kwds.get("relaxed")
  182. if digest_size is not None:
  183. if isinstance(digest_size, uh.native_string_types):
  184. digest_size = int(digest_size)
  185. # NOTE: this isn't *really* digest size minimum, but want to enforce secure minimum.
  186. subcls.checksum_size = uh.norm_integer(subcls, digest_size, min=16, max=MAX_UINT32,
  187. param="digest_size", relaxed=relaxed)
  188. # set memory cost
  189. if memory_cost is not None:
  190. if isinstance(memory_cost, uh.native_string_types):
  191. memory_cost = int(memory_cost)
  192. subcls.memory_cost = subcls._norm_memory_cost(memory_cost, relaxed=relaxed)
  193. # validate constraints
  194. subcls._validate_constraints(subcls.memory_cost, subcls.parallelism)
  195. # set max threads
  196. if max_threads is not None:
  197. if isinstance(max_threads, uh.native_string_types):
  198. max_threads = int(max_threads)
  199. if max_threads < 1 and max_threads != -1:
  200. raise ValueError("max_threads (%d) must be -1 (unlimited), or at least 1." %
  201. (max_threads,))
  202. subcls.max_threads = max_threads
  203. return subcls
  204. @classmethod
  205. def _validate_constraints(cls, memory_cost, parallelism):
  206. # NOTE: this is used by class & instance, hence passing in via arguments.
  207. # could switch and make this a hybrid method.
  208. min_memory_cost = 8 * parallelism
  209. if memory_cost < min_memory_cost:
  210. raise ValueError("%s: memory_cost (%d) is too low, must be at least "
  211. "8 * parallelism (8 * %d = %d)" %
  212. (cls.name, memory_cost,
  213. parallelism, min_memory_cost))
  214. #===================================================================
  215. # public api
  216. #===================================================================
  217. @classmethod
  218. def identify(cls, hash):
  219. hash = uh.to_unicode_for_identify(hash)
  220. return hash.startswith(cls.ident_values)
  221. # hash(), verify(), genhash() -- implemented by backend subclass
  222. #===================================================================
  223. # hash parsing / rendering
  224. #===================================================================
  225. # info taken from source of decode_string() function in
  226. # <https://github.com/P-H-C/phc-winner-argon2/blob/master/src/encoding.c>
  227. #
  228. # hash format:
  229. # $argon2<T>[$v=<num>]$m=<num>,t=<num>,p=<num>[,keyid=<bin>][,data=<bin>][$<bin>[$<bin>]]
  230. #
  231. # NOTE: as of 2016-6-17, the official source (above) lists the "keyid" param in the comments,
  232. # but the actual source of decode_string & encode_string don't mention it at all.
  233. # we're supporting parsing it, but throw NotImplementedError if encountered.
  234. #
  235. # sample hashes:
  236. # v1.0: '$argon2i$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'
  237. # v1.3: '$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'
  238. #: regex to parse argon hash
  239. _hash_regex = re.compile(br"""
  240. ^
  241. \$argon2(?P<type>[id])\$
  242. (?:
  243. v=(?P<version>\d+)
  244. \$
  245. )?
  246. m=(?P<memory_cost>\d+)
  247. ,
  248. t=(?P<time_cost>\d+)
  249. ,
  250. p=(?P<parallelism>\d+)
  251. (?:
  252. ,keyid=(?P<keyid>[^,$]+)
  253. )?
  254. (?:
  255. ,data=(?P<data>[^,$]+)
  256. )?
  257. (?:
  258. \$
  259. (?P<salt>[^$]+)
  260. (?:
  261. \$
  262. (?P<digest>.+)
  263. )?
  264. )?
  265. $
  266. """, re.X)
  267. @classmethod
  268. def from_string(cls, hash):
  269. # NOTE: assuming hash will be unicode, or use ascii-compatible encoding.
  270. if isinstance(hash, unicode):
  271. hash = hash.encode("utf-8")
  272. if not isinstance(hash, bytes):
  273. raise exc.ExpectedStringError(hash, "hash")
  274. m = cls._hash_regex.match(hash)
  275. if not m:
  276. raise exc.MalformedHashError(cls)
  277. type, version, memory_cost, time_cost, parallelism, keyid, data, salt, digest = \
  278. m.group("type", "version", "memory_cost", "time_cost", "parallelism",
  279. "keyid", "data", "salt", "digest")
  280. assert type in [b"i", b"d"], "unexpected type code: %r" % (type,)
  281. if keyid:
  282. raise NotImplementedError("argon2 'keyid' parameter not supported")
  283. return cls(
  284. type_d=(type == b"d"),
  285. version=int(version) if version else 0x10,
  286. memory_cost=int(memory_cost),
  287. rounds=int(time_cost),
  288. parallelism=int(parallelism),
  289. salt=b64s_decode(salt) if salt else None,
  290. data=b64s_decode(data) if data else None,
  291. checksum=b64s_decode(digest) if digest else None,
  292. )
  293. def to_string(self):
  294. ident = str(self.ident_values[self.type_d])
  295. version = self.version
  296. if version == 0x10:
  297. vstr = ""
  298. else:
  299. vstr = "v=%d$" % version
  300. data = self.data
  301. if data:
  302. kdstr = ",data=" + bascii_to_str(b64s_encode(self.data))
  303. else:
  304. kdstr = ""
  305. # NOTE: 'keyid' param currently not supported
  306. return "%s%sm=%d,t=%d,p=%d%s$%s$%s" % (ident, vstr, self.memory_cost,
  307. self.rounds, self.parallelism,
  308. kdstr,
  309. bascii_to_str(b64s_encode(self.salt)),
  310. bascii_to_str(b64s_encode(self.checksum)))
  311. #===================================================================
  312. # init
  313. #===================================================================
  314. def __init__(self, type_d=False, version=None, memory_cost=None, data=None, **kwds):
  315. # TODO: factor out variable checksum size support into a mixin.
  316. # set checksum size to specific value before _norm_checksum() is called
  317. checksum = kwds.get("checksum")
  318. if checksum is not None:
  319. self.checksum_size = len(checksum)
  320. # call parent
  321. super(_Argon2Common, self).__init__(**kwds)
  322. # init type
  323. # NOTE: we don't support *generating* type I hashes, but do support verifying them.
  324. self.type_d = type_d
  325. # init version
  326. if version is None:
  327. assert uh.validate_default_value(self, self.version, self._norm_version,
  328. param="version")
  329. else:
  330. self.version = self._norm_version(version)
  331. # init memory cost
  332. if memory_cost is None:
  333. assert uh.validate_default_value(self, self.memory_cost, self._norm_memory_cost,
  334. param="memory_cost")
  335. else:
  336. self.memory_cost = self._norm_memory_cost(memory_cost)
  337. # init data
  338. if data is None:
  339. assert self.data is None
  340. else:
  341. if not isinstance(data, bytes):
  342. raise uh.exc.ExpectedTypeError(data, "bytes", "data")
  343. self.data = data
  344. #-------------------------------------------------------------------
  345. # parameter guards
  346. #-------------------------------------------------------------------
  347. @classmethod
  348. def _norm_version(cls, version):
  349. if not isinstance(version, uh.int_types):
  350. raise uh.exc.ExpectedTypeError(version, "integer", "version")
  351. # minimum valid version
  352. if version < 0x13 and version != 0x10:
  353. raise ValueError("invalid argon2 hash version: %d" % (version,))
  354. # check this isn't past backend's max version
  355. backend = cls.get_backend()
  356. if version > cls.max_version:
  357. raise ValueError("%s: hash version 0x%X not supported by %r backend "
  358. "(max version is 0x%X); try updating or switching backends" %
  359. (cls.name, version, backend, cls.max_version))
  360. return version
  361. @classmethod
  362. def _norm_memory_cost(cls, memory_cost, relaxed=False):
  363. return uh.norm_integer(cls, memory_cost, min=cls.min_memory_cost,
  364. param="memory_cost", relaxed=relaxed)
  365. #===================================================================
  366. # digest calculation
  367. #===================================================================
  368. # NOTE: _calc_checksum implemented by backend subclass
  369. #===================================================================
  370. # hash migration
  371. #===================================================================
  372. def _calc_needs_update(self, **kwds):
  373. cls = type(self)
  374. if self.type_d:
  375. # type 'd' hashes shouldn't be used for passwords.
  376. return True
  377. minver = cls.min_desired_version
  378. if minver is None or minver > cls.max_version:
  379. minver = cls.max_version
  380. if self.version < minver:
  381. # version is too old.
  382. return True
  383. if self.memory_cost != cls.memory_cost:
  384. return True
  385. if self.checksum_size != cls.checksum_size:
  386. return True
  387. return super(_Argon2Common, self)._calc_needs_update(**kwds)
  388. #===================================================================
  389. # backend loading
  390. #===================================================================
  391. _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install argon2_cffi')"
  392. @classmethod
  393. def _finalize_backend_mixin(mixin_cls, name, dryrun):
  394. """
  395. helper called by from backend mixin classes' _load_backend_mixin() --
  396. invoked after backend imports have been loaded, and performs
  397. feature detection & testing common to all backends.
  398. """
  399. max_version = mixin_cls.max_version
  400. assert isinstance(max_version, int) and max_version >= 0x10
  401. if max_version < 0x13:
  402. warn("%r doesn't support argon2 v1.3, and should be upgraded" % name,
  403. uh.exc.PasslibSecurityWarning)
  404. return True
  405. @classmethod
  406. def _adapt_backend_error(cls, err, hash=None, self=None):
  407. """
  408. internal helper invoked when backend has hash/verification error;
  409. used to adapt to passlib message.
  410. """
  411. backend = cls.get_backend()
  412. # parse hash to throw error if format was invalid, parameter out of range, etc.
  413. if self is None and hash is not None:
  414. self = cls.from_string(hash)
  415. # check constraints on parsed object
  416. # XXX: could move this to __init__, but not needed by needs_update calls
  417. if self is not None:
  418. self._validate_constraints(self.memory_cost, self.parallelism)
  419. # as of cffi 16.1, lacks support in hash_secret(), so genhash() will get here.
  420. # as of cffi 16.2, support removed from verify_secret() as well.
  421. if backend == "argon2_cffi" and self.data is not None:
  422. raise NotImplementedError("argon2_cffi backend doesn't support the 'data' parameter")
  423. # fallback to reporting a malformed hash
  424. text = str(err)
  425. if text not in [
  426. "Decoding failed" # argon2_cffi's default message
  427. ]:
  428. reason = "%s reported: %s: hash=%r" % (backend, text, hash)
  429. else:
  430. reason = repr(hash)
  431. raise exc.MalformedHashError(cls, reason=reason)
  432. #===================================================================
  433. # eoc
  434. #===================================================================
  435. #-----------------------------------------------------------------------
  436. # stub backend
  437. #-----------------------------------------------------------------------
  438. class _NoBackend(_Argon2Common):
  439. """
  440. mixin used before any backend has been loaded.
  441. contains stubs that force loading of one of the available backends.
  442. """
  443. #===================================================================
  444. # primary methods
  445. #===================================================================
  446. @classmethod
  447. def hash(cls, secret):
  448. cls._stub_requires_backend()
  449. return cls.hash(secret)
  450. @classmethod
  451. def verify(cls, secret, hash):
  452. cls._stub_requires_backend()
  453. return cls.verify(secret, hash)
  454. @uh.deprecated_method(deprecated="1.7", removed="2.0")
  455. @classmethod
  456. def genhash(cls, secret, config):
  457. cls._stub_requires_backend()
  458. return cls.genhash(secret, config)
  459. #===================================================================
  460. # digest calculation
  461. #===================================================================
  462. def _calc_checksum(self, secret):
  463. # NOTE: since argon2_cffi takes care of rendering hash,
  464. # _calc_checksum() is only used by the argon2pure backend.
  465. self._stub_requires_backend()
  466. # NOTE: have to use super() here so that we don't recursively
  467. # call subclass's wrapped _calc_checksum
  468. return super(argon2, self)._calc_checksum(secret)
  469. #===================================================================
  470. # eoc
  471. #===================================================================
  472. #-----------------------------------------------------------------------
  473. # argon2_cffi backend
  474. #-----------------------------------------------------------------------
  475. class _CffiBackend(_Argon2Common):
  476. """
  477. argon2_cffi backend
  478. """
  479. #===================================================================
  480. # backend loading
  481. #===================================================================
  482. @classmethod
  483. def _load_backend_mixin(mixin_cls, name, dryrun):
  484. # we automatically import this at top, so just grab info
  485. if _argon2_cffi is None:
  486. return False
  487. max_version = _argon2_cffi.low_level.ARGON2_VERSION
  488. log.debug("detected 'argon2_cffi' backend, version %r, with support for 0x%x argon2 hashes",
  489. _argon2_cffi.__version__, max_version)
  490. mixin_cls.version = mixin_cls.max_version = max_version
  491. return mixin_cls._finalize_backend_mixin(name, dryrun)
  492. #===================================================================
  493. # primary methods
  494. #===================================================================
  495. @classmethod
  496. def hash(cls, secret):
  497. # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.
  498. uh.validate_secret(secret)
  499. secret = to_bytes(secret, "utf-8")
  500. # XXX: doesn't seem to be a way to make this honor max_threads
  501. try:
  502. return bascii_to_str(_argon2_cffi.low_level.hash_secret(
  503. type=_argon2_cffi.low_level.Type.I,
  504. memory_cost=cls.memory_cost,
  505. time_cost=cls.default_rounds,
  506. parallelism=cls.parallelism,
  507. salt=to_bytes(cls._generate_salt()),
  508. hash_len=cls.checksum_size,
  509. secret=secret,
  510. ))
  511. except _argon2_cffi.exceptions.HashingError as err:
  512. raise cls._adapt_backend_error(err)
  513. @classmethod
  514. def verify(cls, secret, hash):
  515. # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.
  516. uh.validate_secret(secret)
  517. secret = to_bytes(secret, "utf-8")
  518. hash = to_bytes(hash, "ascii")
  519. if hash.startswith(b"$argon2d$"):
  520. type = _argon2_cffi.low_level.Type.D
  521. else:
  522. type = _argon2_cffi.low_level.Type.I
  523. # XXX: doesn't seem to be a way to make this honor max_threads
  524. try:
  525. result = _argon2_cffi.low_level.verify_secret(hash, secret, type)
  526. assert result is True
  527. return True
  528. except _argon2_cffi.exceptions.VerifyMismatchError:
  529. return False
  530. except _argon2_cffi.exceptions.VerificationError as err:
  531. raise cls._adapt_backend_error(err, hash=hash)
  532. # NOTE: deprecated, will be removed in 2.0
  533. @classmethod
  534. def genhash(cls, secret, config):
  535. # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.
  536. uh.validate_secret(secret)
  537. secret = to_bytes(secret, "utf-8")
  538. self = cls.from_string(config)
  539. if self.type_d:
  540. type = _argon2_cffi.low_level.Type.D
  541. else:
  542. type = _argon2_cffi.low_level.Type.I
  543. # XXX: doesn't seem to be a way to make this honor max_threads
  544. try:
  545. result = bascii_to_str(_argon2_cffi.low_level.hash_secret(
  546. type=type,
  547. memory_cost=self.memory_cost,
  548. time_cost=self.rounds,
  549. parallelism=self.parallelism,
  550. salt=to_bytes(self.salt),
  551. hash_len=self.checksum_size,
  552. secret=secret,
  553. version=self.version,
  554. ))
  555. except _argon2_cffi.exceptions.HashingError as err:
  556. raise cls._adapt_backend_error(err, hash=config)
  557. if self.version == 0x10:
  558. # workaround: argon2 0x13 always returns "v=" segment, even for 0x10 hashes
  559. result = result.replace("$v=16$", "$")
  560. return result
  561. #===================================================================
  562. # digest calculation
  563. #===================================================================
  564. def _calc_checksum(self, secret):
  565. raise AssertionError("shouldn't be called under argon2_cffi backend")
  566. #===================================================================
  567. # eoc
  568. #===================================================================
  569. #-----------------------------------------------------------------------
  570. # argon2pure backend
  571. #-----------------------------------------------------------------------
  572. class _PureBackend(_Argon2Common):
  573. """
  574. argon2pure backend
  575. """
  576. #===================================================================
  577. # backend loading
  578. #===================================================================
  579. @classmethod
  580. def _load_backend_mixin(mixin_cls, name, dryrun):
  581. # import argon2pure
  582. global _argon2pure
  583. try:
  584. import argon2pure as _argon2pure
  585. except ImportError:
  586. return False
  587. # get default / max supported version -- added in v1.2.2
  588. try:
  589. from argon2pure import ARGON2_DEFAULT_VERSION as max_version
  590. except ImportError:
  591. log.warning("detected 'argon2pure' backend, but package is too old "
  592. "(passlib requires argon2pure >= 1.2.3)")
  593. return False
  594. log.debug("detected 'argon2pure' backend, with support for 0x%x argon2 hashes",
  595. max_version)
  596. if not dryrun:
  597. warn("Using argon2pure backend, which is 100x+ slower than is required "
  598. "for adequate security. Installing argon2_cffi (via 'pip install argon2_cffi') "
  599. "is strongly recommended", exc.PasslibSecurityWarning)
  600. mixin_cls.version = mixin_cls.max_version = max_version
  601. return mixin_cls._finalize_backend_mixin(name, dryrun)
  602. #===================================================================
  603. # primary methods
  604. #===================================================================
  605. # NOTE: this backend uses default .hash() & .verify() implementations.
  606. #===================================================================
  607. # digest calculation
  608. #===================================================================
  609. def _calc_checksum(self, secret):
  610. # TODO: add in 'encoding' support once that's finalized in 1.8 / 1.9.
  611. uh.validate_secret(secret)
  612. secret = to_bytes(secret, "utf-8")
  613. if self.type_d:
  614. type = _argon2pure.ARGON2D
  615. else:
  616. type = _argon2pure.ARGON2I
  617. kwds = dict(
  618. password=secret,
  619. salt=self.salt,
  620. time_cost=self.rounds,
  621. memory_cost=self.memory_cost,
  622. parallelism=self.parallelism,
  623. tag_length=self.checksum_size,
  624. type_code=type,
  625. version=self.version,
  626. )
  627. if self.max_threads > 0:
  628. kwds['threads'] = self.max_threads
  629. if self.pure_use_threads:
  630. kwds['use_threads'] = True
  631. if self.data:
  632. kwds['associated_data'] = self.data
  633. # NOTE: should return raw bytes
  634. # NOTE: this may raise _argon2pure.Argon2ParameterError,
  635. # but it if does that, there's a bug in our own parameter checking code.
  636. try:
  637. return _argon2pure.argon2(**kwds)
  638. except _argon2pure.Argon2Error as err:
  639. raise self._adapt_backend_error(err, self=self)
  640. #===================================================================
  641. # eoc
  642. #===================================================================
  643. class argon2(_NoBackend, _Argon2Common):
  644. """
  645. This class implements the Argon2 password hash [#argon2-home]_, and follows the :ref:`password-hash-api`.
  646. (This class only supports generating "Type I" argon2 hashes).
  647. Argon2 supports a variable-length salt, and variable time & memory cost,
  648. and a number of other configurable parameters.
  649. The :meth:`~passlib.ifc.PasswordHash.replace` method accepts the following optional keywords:
  650. :type salt: str
  651. :param salt:
  652. Optional salt string.
  653. If specified, the length must be between 0-1024 bytes.
  654. If not specified, one will be auto-generated (this is recommended).
  655. :type salt_size: int
  656. :param salt_size:
  657. Optional number of bytes to use when autogenerating new salts.
  658. :type rounds: int
  659. :param rounds:
  660. Optional number of rounds to use.
  661. This corresponds linearly to the amount of time hashing will take.
  662. :type time_cost: int
  663. :param time_cost:
  664. An alias for **rounds**, for compatibility with underlying argon2 library.
  665. :param int memory_cost:
  666. Defines the memory usage in kibibytes.
  667. This corresponds linearly to the amount of memory hashing will take.
  668. :param int parallelism:
  669. Defines the parallelization factor.
  670. *NOTE: this will affect the resulting hash value.*
  671. :param int digest_size:
  672. Length of the digest in bytes.
  673. :param int max_threads:
  674. Maximum number of threads that will be used.
  675. -1 means unlimited; otherwise hashing will use ``min(parallelism, max_threads)`` threads.
  676. .. note::
  677. This option is currently only honored by the argon2pure backend.
  678. :type relaxed: bool
  679. :param relaxed:
  680. By default, providing an invalid value for one of the other
  681. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  682. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  683. will be issued instead. Correctable errors include ``rounds``
  684. that are too small or too large, and ``salt`` strings that are too long.
  685. .. todo::
  686. * Support configurable threading limits.
  687. """
  688. #=============================================================================
  689. # backend
  690. #=============================================================================
  691. # NOTE: the brunt of the argon2 class is implemented in _Argon2Common.
  692. # there are then subclass for each backend (e.g. _PureBackend),
  693. # these are dynamically prepended to this class's bases
  694. # in order to load the appropriate backend.
  695. #: list of potential backends
  696. backends = ("argon2_cffi", "argon2pure")
  697. #: flag that this class's bases should be modified by SubclassBackendMixin
  698. _backend_mixin_target = True
  699. #: map of backend -> mixin class, used by _get_backend_loader()
  700. _backend_mixin_map = {
  701. None: _NoBackend,
  702. "argon2_cffi": _CffiBackend,
  703. "argon2pure": _PureBackend,
  704. }
  705. #=============================================================================
  706. #
  707. #=============================================================================
  708. #=============================================================================
  709. # eof
  710. #=============================================================================