您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

1020 行
41 KiB

  1. """passlib.bcrypt -- implementation of OpenBSD's BCrypt algorithm.
  2. TODO:
  3. * support 2x and altered-2a hashes?
  4. http://www.openwall.com/lists/oss-security/2011/06/27/9
  5. * deal with lack of PY3-compatibile c-ext implementation
  6. """
  7. #=============================================================================
  8. # imports
  9. #=============================================================================
  10. from __future__ import with_statement, absolute_import
  11. # core
  12. from base64 import b64encode
  13. from hashlib import sha256
  14. import os
  15. import re
  16. import logging; log = logging.getLogger(__name__)
  17. from warnings import warn
  18. # site
  19. _bcrypt = None # dynamically imported by _load_backend_bcrypt()
  20. _pybcrypt = None # dynamically imported by _load_backend_pybcrypt()
  21. _bcryptor = None # dynamically imported by _load_backend_bcryptor()
  22. # pkg
  23. _builtin_bcrypt = None # dynamically imported by _load_backend_builtin()
  24. from passlib.exc import PasslibHashWarning, PasslibSecurityWarning, PasslibSecurityError
  25. from passlib.utils import safe_crypt, repeat_string, to_bytes, parse_version, \
  26. rng, getrandstr, test_crypt, to_unicode
  27. from passlib.utils.binary import bcrypt64
  28. from passlib.utils.compat import u, uascii_to_str, unicode, str_to_uascii
  29. import passlib.utils.handlers as uh
  30. # local
  31. __all__ = [
  32. "bcrypt",
  33. ]
  34. #=============================================================================
  35. # support funcs & constants
  36. #=============================================================================
  37. IDENT_2 = u("$2$")
  38. IDENT_2A = u("$2a$")
  39. IDENT_2X = u("$2x$")
  40. IDENT_2Y = u("$2y$")
  41. IDENT_2B = u("$2b$")
  42. _BNULL = b'\x00'
  43. # reference hash of "test", used in various self-checks
  44. TEST_HASH_2A = b"$2a$04$5BJqKfqMQvV7nS.yUguNcueVirQqDBGaLXSqj.rs.pZPlNR0UX/HK"
  45. def _detect_pybcrypt():
  46. """
  47. internal helper which tries to distinguish pybcrypt vs bcrypt.
  48. :returns:
  49. True if cext-based py-bcrypt,
  50. False if ffi-based bcrypt,
  51. None if 'bcrypt' module not found.
  52. .. versionchanged:: 1.6.3
  53. Now assuming bcrypt installed, unless py-bcrypt explicitly detected.
  54. Previous releases assumed py-bcrypt by default.
  55. Making this change since py-bcrypt is (apparently) unmaintained and static,
  56. whereas bcrypt is being actively maintained, and it's internal structure may shift.
  57. """
  58. # NOTE: this is also used by the unittests.
  59. # check for module.
  60. try:
  61. import bcrypt
  62. except ImportError:
  63. return None
  64. # py-bcrypt has a "._bcrypt.__version__" attribute (confirmed for v0.1 - 0.4),
  65. # which bcrypt lacks (confirmed for v1.0 - 2.0)
  66. # "._bcrypt" alone isn't sufficient, since bcrypt 2.0 now has that attribute.
  67. try:
  68. from bcrypt._bcrypt import __version__
  69. except ImportError:
  70. return False
  71. return True
  72. #=============================================================================
  73. # backend mixins
  74. #=============================================================================
  75. class _BcryptCommon(uh.SubclassBackendMixin, uh.TruncateMixin, uh.HasManyIdents,
  76. uh.HasRounds, uh.HasSalt, uh.GenericHandler):
  77. """
  78. Base class which implements brunt of BCrypt code.
  79. This is then subclassed by the various backends,
  80. to override w/ backend-specific methods.
  81. When a backend is loaded, the bases of the 'bcrypt' class proper
  82. are modified to prepend the correct backend-specific subclass.
  83. """
  84. #===================================================================
  85. # class attrs
  86. #===================================================================
  87. #--------------------
  88. # PasswordHash
  89. #--------------------
  90. name = "bcrypt"
  91. setting_kwds = ("salt", "rounds", "ident", "truncate_error")
  92. #--------------------
  93. # GenericHandler
  94. #--------------------
  95. checksum_size = 31
  96. checksum_chars = bcrypt64.charmap
  97. #--------------------
  98. # HasManyIdents
  99. #--------------------
  100. default_ident = IDENT_2B
  101. ident_values = (IDENT_2, IDENT_2A, IDENT_2X, IDENT_2Y, IDENT_2B)
  102. ident_aliases = {u("2"): IDENT_2, u("2a"): IDENT_2A, u("2y"): IDENT_2Y,
  103. u("2b"): IDENT_2B}
  104. #--------------------
  105. # HasSalt
  106. #--------------------
  107. min_salt_size = max_salt_size = 22
  108. salt_chars = bcrypt64.charmap
  109. # NOTE: 22nd salt char must be in bcrypt64._padinfo2[1], not full charmap
  110. #--------------------
  111. # HasRounds
  112. #--------------------
  113. default_rounds = 12 # current passlib default
  114. min_rounds = 4 # minimum from bcrypt specification
  115. max_rounds = 31 # 32-bit integer limit (since real_rounds=1<<rounds)
  116. rounds_cost = "log2"
  117. #--------------------
  118. # TruncateMixin
  119. #--------------------
  120. truncate_size = 72
  121. #--------------------
  122. # custom
  123. #--------------------
  124. # backend workaround detection flags
  125. # NOTE: these are only set on the backend mixin classes
  126. _workrounds_initialized = False
  127. _has_2a_wraparound_bug = False
  128. _lacks_20_support = False
  129. _lacks_2y_support = False
  130. _lacks_2b_support = False
  131. _fallback_ident = IDENT_2A
  132. #===================================================================
  133. # formatting
  134. #===================================================================
  135. @classmethod
  136. def from_string(cls, hash):
  137. ident, tail = cls._parse_ident(hash)
  138. if ident == IDENT_2X:
  139. raise ValueError("crypt_blowfish's buggy '2x' hashes are not "
  140. "currently supported")
  141. rounds_str, data = tail.split(u("$"))
  142. rounds = int(rounds_str)
  143. if rounds_str != u('%02d') % (rounds,):
  144. raise uh.exc.MalformedHashError(cls, "malformed cost field")
  145. salt, chk = data[:22], data[22:]
  146. return cls(
  147. rounds=rounds,
  148. salt=salt,
  149. checksum=chk or None,
  150. ident=ident,
  151. )
  152. def to_string(self):
  153. hash = u("%s%02d$%s%s") % (self.ident, self.rounds, self.salt, self.checksum)
  154. return uascii_to_str(hash)
  155. # NOTE: this should be kept separate from to_string()
  156. # so that bcrypt_sha256() can still use it, while overriding to_string()
  157. def _get_config(self, ident):
  158. """internal helper to prepare config string for backends"""
  159. config = u("%s%02d$%s") % (ident, self.rounds, self.salt)
  160. return uascii_to_str(config)
  161. #===================================================================
  162. # migration
  163. #===================================================================
  164. @classmethod
  165. def needs_update(cls, hash, **kwds):
  166. # check for incorrect padding bits (passlib issue 25)
  167. if isinstance(hash, bytes):
  168. hash = hash.decode("ascii")
  169. if hash.startswith(IDENT_2A) and hash[28] not in bcrypt64._padinfo2[1]:
  170. return True
  171. # TODO: try to detect incorrect 8bit/wraparound hashes using kwds.get("secret")
  172. # hand off to base implementation, so HasRounds can check rounds value.
  173. return super(_BcryptCommon, cls).needs_update(hash, **kwds)
  174. #===================================================================
  175. # specialized salt generation - fixes passlib issue 25
  176. #===================================================================
  177. @classmethod
  178. def normhash(cls, hash):
  179. """helper to normalize hash, correcting any bcrypt padding bits"""
  180. if cls.identify(hash):
  181. return cls.from_string(hash).to_string()
  182. else:
  183. return hash
  184. @classmethod
  185. def _generate_salt(cls):
  186. # generate random salt as normal,
  187. # but repair last char so the padding bits always decode to zero.
  188. salt = super(_BcryptCommon, cls)._generate_salt()
  189. return bcrypt64.repair_unused(salt)
  190. @classmethod
  191. def _norm_salt(cls, salt, **kwds):
  192. salt = super(_BcryptCommon, cls)._norm_salt(salt, **kwds)
  193. assert salt is not None, "HasSalt didn't generate new salt!"
  194. changed, salt = bcrypt64.check_repair_unused(salt)
  195. if changed:
  196. # FIXME: if salt was provided by user, this message won't be
  197. # correct. not sure if we want to throw error, or use different warning.
  198. warn(
  199. "encountered a bcrypt salt with incorrectly set padding bits; "
  200. "you may want to use bcrypt.normhash() "
  201. "to fix this; this will be an error under Passlib 2.0",
  202. PasslibHashWarning)
  203. return salt
  204. def _norm_checksum(self, checksum, relaxed=False):
  205. checksum = super(_BcryptCommon, self)._norm_checksum(checksum, relaxed=relaxed)
  206. changed, checksum = bcrypt64.check_repair_unused(checksum)
  207. if changed:
  208. warn(
  209. "encountered a bcrypt hash with incorrectly set padding bits; "
  210. "you may want to use bcrypt.normhash() "
  211. "to fix this; this will be an error under Passlib 2.0",
  212. PasslibHashWarning)
  213. return checksum
  214. #===================================================================
  215. # backend configuration
  216. # NOTE: backends are defined in terms of mixin classes,
  217. # which are dynamically inserted into the bases of the 'bcrypt' class
  218. # via the machinery in 'SubclassBackendMixin'.
  219. # this lets us load in a backend-specific implementation
  220. # of _calc_checksum() and similar methods.
  221. #===================================================================
  222. # NOTE: backend config is located down in <bcrypt> class
  223. # NOTE: set_backend() will execute the ._load_backend_mixin()
  224. # of the matching mixin class, which will handle backend detection
  225. # appended to HasManyBackends' "no backends available" error message
  226. _no_backend_suggestion = " -- recommend you install one (e.g. 'pip install bcrypt')"
  227. @classmethod
  228. def _finalize_backend_mixin(mixin_cls, backend, dryrun):
  229. """
  230. helper called by from backend mixin classes' _load_backend_mixin() --
  231. invoked after backend imports have been loaded, and performs
  232. feature detection & testing common to all backends.
  233. """
  234. #----------------------------------------------------------------
  235. # setup helpers
  236. #----------------------------------------------------------------
  237. assert mixin_cls is bcrypt._backend_mixin_map[backend], \
  238. "_configure_workarounds() invoked from wrong class"
  239. if mixin_cls._workrounds_initialized:
  240. return True
  241. verify = mixin_cls.verify
  242. err_types = (ValueError,)
  243. if _bcryptor:
  244. err_types += (_bcryptor.engine.SaltError,)
  245. def safe_verify(secret, hash):
  246. """verify() wrapper which traps 'unknown identifier' errors"""
  247. try:
  248. return verify(secret, hash)
  249. except err_types:
  250. # backends without support for given ident will throw various
  251. # errors about unrecognized version:
  252. # pybcrypt, bcrypt -- raises ValueError
  253. # bcryptor -- raises bcryptor.engine.SaltError
  254. return NotImplemented
  255. except AssertionError as err:
  256. # _calc_checksum() code may also throw AssertionError
  257. # if correct hash isn't returned (e.g. 2y hash converted to 2b,
  258. # such as happens with bcrypt 3.0.0)
  259. log.debug("trapped unexpected response from %r backend: verify(%r, %r):",
  260. backend, secret, hash, exc_info=True)
  261. return NotImplemented
  262. def assert_lacks_8bit_bug(ident):
  263. """
  264. helper to check for cryptblowfish 8bit bug (fixed in 2y/2b);
  265. even though it's not known to be present in any of passlib's backends.
  266. this is treated as FATAL, because it can easily result in seriously malformed hashes,
  267. and we can't correct for it ourselves.
  268. test cases from <http://cvsweb.openwall.com/cgi/cvsweb.cgi/Owl/packages/glibc/crypt_blowfish/wrapper.c.diff?r1=1.9;r2=1.10>
  269. reference hash is the incorrectly generated $2x$ hash taken from above url
  270. """
  271. secret = b"\xA3"
  272. bug_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"
  273. if verify(secret, bug_hash):
  274. # NOTE: this only EVER be observed in 2a hashes,
  275. # 2y/2b hashes should have fixed the bug.
  276. # (but we check w/ them anyways).
  277. raise PasslibSecurityError(
  278. "passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "
  279. "the crypt_blowfish 8-bit bug (CVE-2011-2483), "
  280. "and should be upgraded or replaced with another backend." % backend)
  281. # if it doesn't have wraparound bug, make sure it *does* handle things
  282. # correctly -- or we're in some weird third case.
  283. correct_hash = ident.encode("ascii") + b"05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq"
  284. if not verify(secret, correct_hash):
  285. raise RuntimeError("%s backend failed to verify %s 8bit hash" % (backend, ident))
  286. def detect_wrap_bug(ident):
  287. """
  288. check for bsd wraparound bug (fixed in 2b)
  289. this is treated as a warning, because it's rare in the field,
  290. and pybcrypt (as of 2015-7-21) is unpatched, but some people may be stuck with it.
  291. test cases from <http://www.openwall.com/lists/oss-security/2012/01/02/4>
  292. NOTE: reference hash is of password "0"*72
  293. NOTE: if in future we need to deliberately create hashes which have this bug,
  294. can use something like 'hashpw(repeat_string(secret[:((1+secret) % 256) or 1]), 72)'
  295. """
  296. # check if it exhibits wraparound bug
  297. secret = (b"0123456789"*26)[:255]
  298. bug_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"
  299. if verify(secret, bug_hash):
  300. return True
  301. # if it doesn't have wraparound bug, make sure it *does* handle things
  302. # correctly -- or we're in some weird third case.
  303. correct_hash = ident.encode("ascii") + b"04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi"
  304. if not verify(secret, correct_hash):
  305. raise RuntimeError("%s backend failed to verify %s wraparound hash" % (backend, ident))
  306. return False
  307. def assert_lacks_wrap_bug(ident):
  308. if not detect_wrap_bug(ident):
  309. return
  310. # should only see in 2a, later idents should NEVER exhibit this bug:
  311. # * 2y implementations should have been free of it
  312. # * 2b was what (supposedly) fixed it
  313. raise RuntimeError("%s backend unexpectedly has wraparound bug for %s" % (backend, ident))
  314. #----------------------------------------------------------------
  315. # check for old 20 support
  316. #----------------------------------------------------------------
  317. test_hash_20 = b"$2$04$5BJqKfqMQvV7nS.yUguNcuRfMMOXK0xPWavM7pOzjEi5ze5T1k8/S"
  318. result = safe_verify("test", test_hash_20)
  319. if not result:
  320. raise RuntimeError("%s incorrectly rejected $2$ hash" % backend)
  321. elif result is NotImplemented:
  322. mixin_cls._lacks_20_support = True
  323. log.debug("%r backend lacks $2$ support, enabling workaround", backend)
  324. #----------------------------------------------------------------
  325. # check for 2a support
  326. #----------------------------------------------------------------
  327. result = safe_verify("test", TEST_HASH_2A)
  328. if not result:
  329. raise RuntimeError("%s incorrectly rejected $2a$ hash" % backend)
  330. elif result is NotImplemented:
  331. # 2a support is required, and should always be present
  332. raise RuntimeError("%s lacks support for $2a$ hashes" % backend)
  333. else:
  334. assert_lacks_8bit_bug(IDENT_2A)
  335. if detect_wrap_bug(IDENT_2A):
  336. warn("passlib.hash.bcrypt: Your installation of the %r backend is vulnerable to "
  337. "the bsd wraparound bug, "
  338. "and should be upgraded or replaced with another backend "
  339. "(enabling workaround for now)." % backend,
  340. uh.exc.PasslibSecurityWarning)
  341. mixin_cls._has_2a_wraparound_bug = True
  342. #----------------------------------------------------------------
  343. # check for 2y support
  344. #----------------------------------------------------------------
  345. test_hash_2y = TEST_HASH_2A.replace(b"2a", b"2y")
  346. result = safe_verify("test", test_hash_2y)
  347. if not result:
  348. raise RuntimeError("%s incorrectly rejected $2y$ hash" % backend)
  349. elif result is NotImplemented:
  350. mixin_cls._lacks_2y_support = True
  351. log.debug("%r backend lacks $2y$ support, enabling workaround", backend)
  352. else:
  353. # NOTE: Not using this as fallback candidate,
  354. # lacks wide enough support across implementations.
  355. assert_lacks_8bit_bug(IDENT_2Y)
  356. assert_lacks_wrap_bug(IDENT_2Y)
  357. #----------------------------------------------------------------
  358. # TODO: check for 2x support
  359. #----------------------------------------------------------------
  360. #----------------------------------------------------------------
  361. # check for 2b support
  362. #----------------------------------------------------------------
  363. test_hash_2b = TEST_HASH_2A.replace(b"2a", b"2b")
  364. result = safe_verify("test", test_hash_2b)
  365. if not result:
  366. raise RuntimeError("%s incorrectly rejected $2b$ hash" % backend)
  367. elif result is NotImplemented:
  368. mixin_cls._lacks_2b_support = True
  369. log.debug("%r backend lacks $2b$ support, enabling workaround", backend)
  370. else:
  371. mixin_cls._fallback_ident = IDENT_2B
  372. assert_lacks_8bit_bug(IDENT_2B)
  373. assert_lacks_wrap_bug(IDENT_2B)
  374. # set flag so we don't have to run this again
  375. mixin_cls._workrounds_initialized = True
  376. return True
  377. #===================================================================
  378. # digest calculation
  379. #===================================================================
  380. # _calc_checksum() defined by backends
  381. def _prepare_digest_args(self, secret):
  382. """
  383. common helper for backends to implement _calc_checksum().
  384. takes in secret, returns (secret, ident) pair,
  385. """
  386. return self._norm_digest_args(secret, self.ident, new=self.use_defaults)
  387. @classmethod
  388. def _norm_digest_args(cls, secret, ident, new=False):
  389. # make sure secret is unicode
  390. if isinstance(secret, unicode):
  391. secret = secret.encode("utf-8")
  392. # check max secret size
  393. uh.validate_secret(secret)
  394. # check for truncation (during .hash() calls only)
  395. if new:
  396. cls._check_truncate_policy(secret)
  397. # NOTE: especially important to forbid NULLs for bcrypt, since many
  398. # backends (bcryptor, bcrypt) happily accept them, and then
  399. # silently truncate the password at first NULL they encounter!
  400. if _BNULL in secret:
  401. raise uh.exc.NullPasswordError(cls)
  402. # TODO: figure out way to skip these tests when not needed...
  403. # protect from wraparound bug by truncating secret before handing it to the backend.
  404. # bcrypt only uses first 72 bytes anyways.
  405. # NOTE: not needed for 2y/2b, but might use 2a as fallback for them.
  406. if cls._has_2a_wraparound_bug and len(secret) >= 255:
  407. secret = secret[:72]
  408. # special case handling for variants (ordered most common first)
  409. if ident == IDENT_2A:
  410. # nothing needs to be done.
  411. pass
  412. elif ident == IDENT_2B:
  413. if cls._lacks_2b_support:
  414. # handle $2b$ hash format even if backend is too old.
  415. # have it generate a 2A/2Y digest, then return it as a 2B hash.
  416. # 2a-only backend could potentially exhibit wraparound bug --
  417. # but we work around that issue above.
  418. ident = cls._fallback_ident
  419. elif ident == IDENT_2Y:
  420. if cls._lacks_2y_support:
  421. # handle $2y$ hash format (not supported by BSDs, being phased out on others)
  422. # have it generate a 2A/2B digest, then return it as a 2Y hash.
  423. ident = cls._fallback_ident
  424. elif ident == IDENT_2:
  425. if cls._lacks_20_support:
  426. # handle legacy $2$ format (not supported by most backends except BSD os_crypt)
  427. # we can fake $2$ behavior using the 2A/2Y/2B algorithm
  428. # by repeating the password until it's at least 72 chars in length.
  429. if secret:
  430. secret = repeat_string(secret, 72)
  431. ident = cls._fallback_ident
  432. elif ident == IDENT_2X:
  433. # NOTE: shouldn't get here.
  434. # XXX: could check if backend does actually offer 'support'
  435. raise RuntimeError("$2x$ hashes not currently supported by passlib")
  436. else:
  437. raise AssertionError("unexpected ident value: %r" % ident)
  438. return secret, ident
  439. #-----------------------------------------------------------------------
  440. # stub backend
  441. #-----------------------------------------------------------------------
  442. class _NoBackend(_BcryptCommon):
  443. """
  444. mixin used before any backend has been loaded.
  445. contains stubs that force loading of one of the available backends.
  446. """
  447. #===================================================================
  448. # digest calculation
  449. #===================================================================
  450. def _calc_checksum(self, secret):
  451. self._stub_requires_backend()
  452. # NOTE: have to use super() here so that we don't recursively
  453. # call subclass's wrapped _calc_checksum, e.g. bcrypt_sha256._calc_checksum
  454. return super(bcrypt, self)._calc_checksum(secret)
  455. #===================================================================
  456. # eoc
  457. #===================================================================
  458. #-----------------------------------------------------------------------
  459. # bcrypt backend
  460. #-----------------------------------------------------------------------
  461. class _BcryptBackend(_BcryptCommon):
  462. """
  463. backend which uses 'bcrypt' package
  464. """
  465. @classmethod
  466. def _load_backend_mixin(mixin_cls, name, dryrun):
  467. # try to import bcrypt
  468. global _bcrypt
  469. if _detect_pybcrypt():
  470. # pybcrypt was installed instead
  471. return False
  472. try:
  473. import bcrypt as _bcrypt
  474. except ImportError: # pragma: no cover
  475. return False
  476. try:
  477. version = _bcrypt.__about__.__version__
  478. except:
  479. log.warning("(trapped) error reading bcrypt version", exc_info=True)
  480. version = '<unknown>'
  481. log.debug("detected 'bcrypt' backend, version %r", version)
  482. return mixin_cls._finalize_backend_mixin(name, dryrun)
  483. # # TODO: would like to implementing verify() directly,
  484. # # to skip need for parsing hash strings.
  485. # # below method has a few edge cases where it chokes though.
  486. # @classmethod
  487. # def verify(cls, secret, hash):
  488. # if isinstance(hash, unicode):
  489. # hash = hash.encode("ascii")
  490. # ident = hash[:hash.index(b"$", 1)+1].decode("ascii")
  491. # if ident not in cls.ident_values:
  492. # raise uh.exc.InvalidHashError(cls)
  493. # secret, eff_ident = cls._norm_digest_args(secret, ident)
  494. # if eff_ident != ident:
  495. # # lacks support for original ident, replace w/ new one.
  496. # hash = eff_ident.encode("ascii") + hash[len(ident):]
  497. # result = _bcrypt.hashpw(secret, hash)
  498. # assert result.startswith(eff_ident)
  499. # return consteq(result, hash)
  500. def _calc_checksum(self, secret):
  501. # bcrypt behavior:
  502. # secret must be bytes
  503. # config must be ascii bytes
  504. # returns ascii bytes
  505. secret, ident = self._prepare_digest_args(secret)
  506. config = self._get_config(ident)
  507. if isinstance(config, unicode):
  508. config = config.encode("ascii")
  509. hash = _bcrypt.hashpw(secret, config)
  510. assert hash.startswith(config) and len(hash) == len(config)+31, \
  511. "config mismatch: %r => %r" % (config, hash)
  512. assert isinstance(hash, bytes)
  513. return hash[-31:].decode("ascii")
  514. #-----------------------------------------------------------------------
  515. # bcryptor backend
  516. #-----------------------------------------------------------------------
  517. class _BcryptorBackend(_BcryptCommon):
  518. """
  519. backend which uses 'bcryptor' package
  520. """
  521. @classmethod
  522. def _load_backend_mixin(mixin_cls, name, dryrun):
  523. # try to import bcryptor
  524. global _bcryptor
  525. try:
  526. import bcryptor as _bcryptor
  527. except ImportError: # pragma: no cover
  528. return False
  529. return mixin_cls._finalize_backend_mixin(name, dryrun)
  530. def _calc_checksum(self, secret):
  531. # bcryptor behavior:
  532. # py2: unicode secret/hash encoded as ascii bytes before use,
  533. # bytes taken as-is; returns ascii bytes.
  534. # py3: not supported
  535. secret, ident = self._prepare_digest_args(secret)
  536. config = self._get_config(ident)
  537. hash = _bcryptor.engine.Engine(False).hash_key(secret, config)
  538. assert hash.startswith(config) and len(hash) == len(config)+31
  539. return str_to_uascii(hash[-31:])
  540. #-----------------------------------------------------------------------
  541. # pybcrypt backend
  542. #-----------------------------------------------------------------------
  543. class _PyBcryptBackend(_BcryptCommon):
  544. """
  545. backend which uses 'pybcrypt' package
  546. """
  547. #: classwide thread lock used for pybcrypt < 0.3
  548. _calc_lock = None
  549. @classmethod
  550. def _load_backend_mixin(mixin_cls, name, dryrun):
  551. # try to import pybcrypt
  552. global _pybcrypt
  553. if not _detect_pybcrypt():
  554. # not installed, or bcrypt installed instead
  555. return False
  556. try:
  557. import bcrypt as _pybcrypt
  558. except ImportError: # pragma: no cover
  559. return False
  560. # determine pybcrypt version
  561. try:
  562. version = _pybcrypt._bcrypt.__version__
  563. except:
  564. log.warning("(trapped) error reading pybcrypt version", exc_info=True)
  565. version = "<unknown>"
  566. log.debug("detected 'pybcrypt' backend, version %r", version)
  567. # return calc function based on version
  568. vinfo = parse_version(version) or (0, 0)
  569. if vinfo < (0, 3):
  570. warn("py-bcrypt %s has a major security vulnerability, "
  571. "you should upgrade to py-bcrypt 0.3 immediately."
  572. % version, uh.exc.PasslibSecurityWarning)
  573. if mixin_cls._calc_lock is None:
  574. import threading
  575. mixin_cls._calc_lock = threading.Lock()
  576. mixin_cls._calc_checksum = mixin_cls._calc_checksum_threadsafe.__func__
  577. return mixin_cls._finalize_backend_mixin(name, dryrun)
  578. def _calc_checksum_threadsafe(self, secret):
  579. # as workaround for pybcrypt < 0.3's concurrency issue,
  580. # we wrap everything in a thread lock. as long as bcrypt is only
  581. # used through passlib, this should be safe.
  582. with self._calc_lock:
  583. return self._calc_checksum_raw(secret)
  584. def _calc_checksum_raw(self, secret):
  585. # py-bcrypt behavior:
  586. # py2: unicode secret/hash encoded as ascii bytes before use,
  587. # bytes taken as-is; returns ascii bytes.
  588. # py3: unicode secret encoded as utf-8 bytes,
  589. # hash encoded as ascii bytes, returns ascii unicode.
  590. secret, ident = self._prepare_digest_args(secret)
  591. config = self._get_config(ident)
  592. hash = _pybcrypt.hashpw(secret, config)
  593. assert hash.startswith(config) and len(hash) == len(config)+31
  594. return str_to_uascii(hash[-31:])
  595. _calc_checksum = _calc_checksum_raw
  596. #-----------------------------------------------------------------------
  597. # os crypt backend
  598. #-----------------------------------------------------------------------
  599. class _OsCryptBackend(_BcryptCommon):
  600. """
  601. backend which uses :func:`crypt.crypt`
  602. """
  603. @classmethod
  604. def _load_backend_mixin(mixin_cls, name, dryrun):
  605. if not test_crypt("test", TEST_HASH_2A):
  606. return False
  607. return mixin_cls._finalize_backend_mixin(name, dryrun)
  608. def _calc_checksum(self, secret):
  609. secret, ident = self._prepare_digest_args(secret)
  610. config = self._get_config(ident)
  611. hash = safe_crypt(secret, config)
  612. if hash:
  613. assert hash.startswith(config) and len(hash) == len(config)+31
  614. return hash[-31:]
  615. else:
  616. # NOTE: Have to raise this error because python3's crypt.crypt() only accepts unicode.
  617. # This means it can't handle any passwords that aren't either unicode
  618. # or utf-8 encoded bytes. However, hashing a password with an alternate
  619. # encoding should be a pretty rare edge case; if user needs it, they can just
  620. # install bcrypt backend.
  621. # XXX: is this the right error type to raise?
  622. # maybe have safe_crypt() not swallow UnicodeDecodeError, and have handlers
  623. # like sha256_crypt trap it if they have alternate method of handling them?
  624. raise uh.exc.MissingBackendError(
  625. "non-utf8 encoded passwords can't be handled by crypt.crypt() under python3, "
  626. "recommend running `pip install bcrypt`.",
  627. )
  628. #-----------------------------------------------------------------------
  629. # builtin backend
  630. #-----------------------------------------------------------------------
  631. class _BuiltinBackend(_BcryptCommon):
  632. """
  633. backend which uses passlib's pure-python implementation
  634. """
  635. @classmethod
  636. def _load_backend_mixin(mixin_cls, name, dryrun):
  637. from passlib.utils import as_bool
  638. if not as_bool(os.environ.get("PASSLIB_BUILTIN_BCRYPT")):
  639. log.debug("bcrypt 'builtin' backend not enabled via $PASSLIB_BUILTIN_BCRYPT")
  640. return False
  641. global _builtin_bcrypt
  642. from passlib.crypto._blowfish import raw_bcrypt as _builtin_bcrypt
  643. return mixin_cls._finalize_backend_mixin(name, dryrun)
  644. def _calc_checksum(self, secret):
  645. secret, ident = self._prepare_digest_args(secret)
  646. chk = _builtin_bcrypt(secret, ident[1:-1],
  647. self.salt.encode("ascii"), self.rounds)
  648. return chk.decode("ascii")
  649. #=============================================================================
  650. # handler
  651. #=============================================================================
  652. class bcrypt(_NoBackend, _BcryptCommon):
  653. """This class implements the BCrypt password hash, and follows the :ref:`password-hash-api`.
  654. It supports a fixed-length salt, and a variable number of rounds.
  655. The :meth:`~passlib.ifc.PasswordHash.using` method accepts the following optional keywords:
  656. :type salt: str
  657. :param salt:
  658. Optional salt string.
  659. If not specified, one will be autogenerated (this is recommended).
  660. If specified, it must be 22 characters, drawn from the regexp range ``[./0-9A-Za-z]``.
  661. :type rounds: int
  662. :param rounds:
  663. Optional number of rounds to use.
  664. Defaults to 12, must be between 4 and 31, inclusive.
  665. This value is logarithmic, the actual number of iterations used will be :samp:`2**{rounds}`
  666. -- increasing the rounds by +1 will double the amount of time taken.
  667. :type ident: str
  668. :param ident:
  669. Specifies which version of the BCrypt algorithm will be used when creating a new hash.
  670. Typically this option is not needed, as the default (``"2b"``) is usually the correct choice.
  671. If specified, it must be one of the following:
  672. * ``"2"`` - the first revision of BCrypt, which suffers from a minor security flaw and is generally not used anymore.
  673. * ``"2a"`` - some implementations suffered from rare security flaws, replaced by 2b.
  674. * ``"2y"`` - format specific to the *crypt_blowfish* BCrypt implementation,
  675. identical to ``"2b"`` in all but name.
  676. * ``"2b"`` - latest revision of the official BCrypt algorithm, current default.
  677. :param bool truncate_error:
  678. By default, BCrypt will silently truncate passwords larger than 72 bytes.
  679. Setting ``truncate_error=True`` will cause :meth:`~passlib.ifc.PasswordHash.hash`
  680. to raise a :exc:`~passlib.exc.PasswordTruncateError` instead.
  681. .. versionadded:: 1.7
  682. :type relaxed: bool
  683. :param relaxed:
  684. By default, providing an invalid value for one of the other
  685. keywords will result in a :exc:`ValueError`. If ``relaxed=True``,
  686. and the error can be corrected, a :exc:`~passlib.exc.PasslibHashWarning`
  687. will be issued instead. Correctable errors include ``rounds``
  688. that are too small or too large, and ``salt`` strings that are too long.
  689. .. versionadded:: 1.6
  690. .. versionchanged:: 1.6
  691. This class now supports ``"2y"`` hashes, and recognizes
  692. (but does not support) the broken ``"2x"`` hashes.
  693. (see the :ref:`crypt_blowfish bug <crypt-blowfish-bug>`
  694. for details).
  695. .. versionchanged:: 1.6
  696. Added a pure-python backend.
  697. .. versionchanged:: 1.6.3
  698. Added support for ``"2b"`` variant.
  699. .. versionchanged:: 1.7
  700. Now defaults to ``"2b"`` variant.
  701. """
  702. #=============================================================================
  703. # backend
  704. #=============================================================================
  705. # NOTE: the brunt of the bcrypt class is implemented in _BcryptCommon.
  706. # there are then subclass for each backend (e.g. _PyBcryptBackend),
  707. # these are dynamically prepended to this class's bases
  708. # in order to load the appropriate backend.
  709. #: list of potential backends
  710. backends = ("bcrypt", "pybcrypt", "bcryptor", "os_crypt", "builtin")
  711. #: flag that this class's bases should be modified by SubclassBackendMixin
  712. _backend_mixin_target = True
  713. #: map of backend -> mixin class, used by _get_backend_loader()
  714. _backend_mixin_map = {
  715. None: _NoBackend,
  716. "bcrypt": _BcryptBackend,
  717. "pybcrypt": _PyBcryptBackend,
  718. "bcryptor": _BcryptorBackend,
  719. "os_crypt": _OsCryptBackend,
  720. "builtin": _BuiltinBackend,
  721. }
  722. #=============================================================================
  723. # eoc
  724. #=============================================================================
  725. #=============================================================================
  726. # variants
  727. #=============================================================================
  728. _UDOLLAR = u("$")
  729. # XXX: it might be better to have all the bcrypt variants share a common base class,
  730. # and have the (django_)bcrypt_sha256 wrappers just proxy bcrypt instead of subclassing it.
  731. class _wrapped_bcrypt(bcrypt):
  732. """
  733. abstracts out some bits bcrypt_sha256 & django_bcrypt_sha256 share.
  734. - bypass backend-loading wrappers for hash() etc
  735. - disable truncation support, sha256 wrappers don't need it.
  736. """
  737. setting_kwds = tuple(elem for elem in bcrypt.setting_kwds if elem not in ["truncate_error"])
  738. truncate_size = None
  739. # XXX: these will be needed if any bcrypt backends directly implement this...
  740. # @classmethod
  741. # def hash(cls, secret, **kwds):
  742. # # bypass bcrypt backend overriding this method
  743. # # XXX: would wrapping bcrypt make this easier than subclassing it?
  744. # return super(_BcryptCommon, cls).hash(secret, **kwds)
  745. #
  746. # @classmethod
  747. # def verify(cls, secret, hash):
  748. # # bypass bcrypt backend overriding this method
  749. # return super(_BcryptCommon, cls).verify(secret, hash)
  750. #
  751. # @classmethod
  752. # def genhash(cls, secret, hash):
  753. # # bypass bcrypt backend overriding this method
  754. # return super(_BcryptCommon, cls).genhash(secret, hash)
  755. @classmethod
  756. def _check_truncate_policy(cls, secret):
  757. # disable check performed by bcrypt(), since this doesn't truncate passwords.
  758. pass
  759. #=============================================================================
  760. # bcrypt sha256 wrapper
  761. #=============================================================================
  762. class bcrypt_sha256(_wrapped_bcrypt):
  763. """This class implements a composition of BCrypt+SHA256, and follows the :ref:`password-hash-api`.
  764. It supports a fixed-length salt, and a variable number of rounds.
  765. The :meth:`~passlib.ifc.PasswordHash.hash` and :meth:`~passlib.ifc.PasswordHash.genconfig` methods accept
  766. all the same optional keywords as the base :class:`bcrypt` hash.
  767. .. versionadded:: 1.6.2
  768. .. versionchanged:: 1.7
  769. Now defaults to ``"2b"`` variant.
  770. """
  771. #===================================================================
  772. # class attrs
  773. #===================================================================
  774. #--------------------
  775. # PasswordHash
  776. #--------------------
  777. name = "bcrypt_sha256"
  778. #--------------------
  779. # GenericHandler
  780. #--------------------
  781. # this is locked at 2a/2b for now.
  782. ident_values = (IDENT_2A, IDENT_2B)
  783. # clone bcrypt's ident aliases so they can be used here as well...
  784. ident_aliases = (lambda ident_values: dict(item for item in bcrypt.ident_aliases.items()
  785. if item[1] in ident_values))(ident_values)
  786. default_ident = IDENT_2B
  787. #===================================================================
  788. # formatting
  789. #===================================================================
  790. # sample hash:
  791. # $bcrypt-sha256$2a,6$/3OeRpbOf8/l6nPPRdZPp.$nRiyYqPobEZGdNRBWihQhiFDh1ws1tu
  792. # $bcrypt-sha256$ -- prefix/identifier
  793. # 2a -- bcrypt variant
  794. # , -- field separator
  795. # 6 -- bcrypt work factor
  796. # $ -- section separator
  797. # /3OeRpbOf8/l6nPPRdZPp. -- salt
  798. # $ -- section separator
  799. # nRiyYqPobEZGdNRBWihQhiFDh1ws1tu -- digest
  800. # XXX: we can't use .ident attr due to bcrypt code using it.
  801. # working around that via prefix.
  802. prefix = u('$bcrypt-sha256$')
  803. _hash_re = re.compile(r"""
  804. ^
  805. [$]bcrypt-sha256
  806. [$](?P<variant>2[ab])
  807. ,(?P<rounds>\d{1,2})
  808. [$](?P<salt>[^$]{22})
  809. (?:[$](?P<digest>.{31}))?
  810. $
  811. """, re.X)
  812. @classmethod
  813. def identify(cls, hash):
  814. hash = uh.to_unicode_for_identify(hash)
  815. if not hash:
  816. return False
  817. return hash.startswith(cls.prefix)
  818. @classmethod
  819. def from_string(cls, hash):
  820. hash = to_unicode(hash, "ascii", "hash")
  821. if not hash.startswith(cls.prefix):
  822. raise uh.exc.InvalidHashError(cls)
  823. m = cls._hash_re.match(hash)
  824. if not m:
  825. raise uh.exc.MalformedHashError(cls)
  826. rounds = m.group("rounds")
  827. if rounds.startswith(uh._UZERO) and rounds != uh._UZERO:
  828. raise uh.exc.ZeroPaddedRoundsError(cls)
  829. return cls(ident=m.group("variant"),
  830. rounds=int(rounds),
  831. salt=m.group("salt"),
  832. checksum=m.group("digest"),
  833. )
  834. _template = u("$bcrypt-sha256$%s,%d$%s$%s")
  835. def to_string(self):
  836. hash = self._template % (self.ident.strip(_UDOLLAR),
  837. self.rounds, self.salt, self.checksum)
  838. return uascii_to_str(hash)
  839. #===================================================================
  840. # checksum
  841. #===================================================================
  842. def _calc_checksum(self, secret):
  843. # NOTE: can't use digest directly, since bcrypt stops at first NULL.
  844. # NOTE: bcrypt doesn't fully mix entropy for bytes 55-72 of password
  845. # (XXX: citation needed), so we don't want key to be > 55 bytes.
  846. # thus, have to use base64 (44 bytes) rather than hex (64 bytes).
  847. # XXX: it's later come out that 55-72 may be ok, so later revision of bcrypt_sha256
  848. # may switch to hex encoding, since it's simpler to implement elsewhere.
  849. if isinstance(secret, unicode):
  850. secret = secret.encode("utf-8")
  851. # NOTE: output of b64encode() uses "+/" altchars, "=" padding chars,
  852. # and no leading/trailing whitespace.
  853. key = b64encode(sha256(secret).digest())
  854. # hand result off to normal bcrypt algorithm
  855. return super(bcrypt_sha256, self)._calc_checksum(key)
  856. #===================================================================
  857. # other
  858. #===================================================================
  859. # XXX: have _needs_update() mark the $2a$ ones for upgrading?
  860. # maybe do that after we switch to hex encoding?
  861. #===================================================================
  862. # eoc
  863. #===================================================================
  864. #=============================================================================
  865. # eof
  866. #=============================================================================