test_handlers_bcrypt.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. """passlib.tests.test_handlers - tests for passlib hash algorithms"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. from __future__ import with_statement
  6. # core
  7. import logging; log = logging.getLogger(__name__)
  8. import os
  9. import warnings
  10. # site
  11. # pkg
  12. from passlib import hash
  13. from passlib.handlers.bcrypt import IDENT_2, IDENT_2X
  14. from passlib.utils import repeat_string, to_bytes, is_safe_crypt_input
  15. from passlib.utils.compat import irange, PY3
  16. from passlib.tests.utils import HandlerCase, TEST_MODE
  17. from passlib.tests.test_handlers import UPASS_TABLE
  18. # module
  19. #=============================================================================
  20. # bcrypt
  21. #=============================================================================
  22. class _bcrypt_test(HandlerCase):
  23. """base for BCrypt test cases"""
  24. handler = hash.bcrypt
  25. reduce_default_rounds = True
  26. fuzz_salts_need_bcrypt_repair = True
  27. known_correct_hashes = [
  28. #
  29. # from JTR 1.7.9
  30. #
  31. ('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'),
  32. ('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'),
  33. ('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'),
  34. ('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'),
  35. ('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'),
  36. #
  37. # test vectors from http://www.openwall.com/crypt v1.2
  38. # note that this omits any hashes that depend on crypt_blowfish's
  39. # various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password,
  40. # and any 2x hashes); and only contain hashes which are correct
  41. # under both crypt_blowfish 1.2 AND OpenBSD.
  42. #
  43. ('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'),
  44. ('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'),
  45. ('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'),
  46. ('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'),
  47. ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  48. '0123456789chars after 72 are ignored',
  49. '$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'),
  50. (b'\xa3',
  51. '$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'),
  52. (b'\xff\xa3345',
  53. '$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'),
  54. (b'\xa3ab',
  55. '$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'),
  56. (b'\xaa'*72 + b'chars after 72 are ignored as usual',
  57. '$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'),
  58. (b'\xaa\x55'*36,
  59. '$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'),
  60. (b'\x55\xaa\xff'*24,
  61. '$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'),
  62. # keeping one of their 2y tests, because we are supporting that.
  63. (b'\xa3',
  64. '$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'),
  65. #
  66. # 8bit bug (fixed in 2y/2b)
  67. #
  68. # NOTE: see assert_lacks_8bit_bug() for origins of this test vector.
  69. (b"\xd1\x91", "$2y$05$6bNw2HLQYeqHYyBfLMsv/OUcZd0LKP39b87nBw3.S2tVZSqiQX6eu"),
  70. #
  71. # bsd wraparound bug (fixed in 2b)
  72. #
  73. # NOTE: if backend is vulnerable, password will hash the same as '0'*72
  74. # ("$2a$04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"),
  75. # rather than same as ("0123456789"*8)[:72]
  76. # 255 should be sufficient, but checking
  77. (("0123456789"*26)[:254], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
  78. (("0123456789"*26)[:255], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
  79. (("0123456789"*26)[:256], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
  80. (("0123456789"*26)[:257], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
  81. #
  82. # from py-bcrypt tests
  83. #
  84. ('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
  85. ('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'),
  86. ('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'),
  87. ('abcdefghijklmnopqrstuvwxyz',
  88. '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
  89. ('~!@#$%^&*() ~!@#$%^&*()PNBFRD',
  90. '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'),
  91. #
  92. # custom test vectors
  93. #
  94. # ensures utf-8 used for unicode
  95. (UPASS_TABLE,
  96. '$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
  97. # ensure 2b support
  98. (UPASS_TABLE,
  99. '$2b$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
  100. ]
  101. if TEST_MODE("full"):
  102. #
  103. # add some extra tests related to 2/2a
  104. #
  105. CONFIG_2 = '$2$05$' + '.'*22
  106. CONFIG_A = '$2a$05$' + '.'*22
  107. known_correct_hashes.extend([
  108. ("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
  109. ("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
  110. ("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
  111. ("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'),
  112. ("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
  113. ("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'),
  114. ("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
  115. ("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
  116. ("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
  117. ("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
  118. ])
  119. known_correct_configs = [
  120. ('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE,
  121. '$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'),
  122. ]
  123. known_unidentified_hashes = [
  124. # invalid minor version
  125. "$2f$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
  126. "$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
  127. ]
  128. known_malformed_hashes = [
  129. # bad char in otherwise correct hash
  130. # \/
  131. "$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
  132. # unsupported (but recognized) minor version
  133. "$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
  134. # rounds not zero-padded (py-bcrypt rejects this, therefore so do we)
  135. '$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'
  136. # NOTE: salts with padding bits set are technically malformed,
  137. # but we can reliably correct & issue a warning for that.
  138. ]
  139. platform_crypt_support = [
  140. ("freedbsd|openbsd|netbsd", True),
  141. ("darwin", False),
  142. ("linux", None), # may be present via addon, e.g. debian's libpam-unix2
  143. ("solaris", None), # depends on system policy
  144. ]
  145. #===================================================================
  146. # override some methods
  147. #===================================================================
  148. def setUp(self):
  149. # ensure builtin is enabled for duration of test.
  150. if TEST_MODE("full") and self.backend == "builtin":
  151. key = "PASSLIB_BUILTIN_BCRYPT"
  152. orig = os.environ.get(key)
  153. if orig:
  154. self.addCleanup(os.environ.__setitem__, key, orig)
  155. else:
  156. self.addCleanup(os.environ.__delitem__, key)
  157. os.environ[key] = "true"
  158. super(_bcrypt_test, self).setUp()
  159. # silence this warning, will come up a bunch during testing of old 2a hashes.
  160. warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*")
  161. def populate_settings(self, kwds):
  162. # builtin is still just way too slow.
  163. if self.backend == "builtin":
  164. kwds.setdefault("rounds", 4)
  165. super(_bcrypt_test, self).populate_settings(kwds)
  166. #===================================================================
  167. # fuzz testing
  168. #===================================================================
  169. def crypt_supports_variant(self, hash):
  170. """check if OS crypt is expected to support given ident"""
  171. from passlib.handlers.bcrypt import bcrypt, IDENT_2X, IDENT_2Y
  172. from passlib.utils import safe_crypt
  173. ident = bcrypt.from_string(hash)
  174. return (safe_crypt("test", ident + "04$5BJqKfqMQvV7nS.yUguNcu") or "").startswith(ident)
  175. fuzz_verifiers = HandlerCase.fuzz_verifiers + (
  176. "fuzz_verifier_bcrypt",
  177. "fuzz_verifier_pybcrypt",
  178. "fuzz_verifier_bcryptor",
  179. )
  180. def fuzz_verifier_bcrypt(self):
  181. # test against bcrypt, if available
  182. from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y, _detect_pybcrypt
  183. from passlib.utils import to_native_str, to_bytes
  184. try:
  185. import bcrypt
  186. except ImportError:
  187. return
  188. if _detect_pybcrypt():
  189. return
  190. def check_bcrypt(secret, hash):
  191. """bcrypt"""
  192. secret = to_bytes(secret, self.FuzzHashGenerator.password_encoding)
  193. if hash.startswith(IDENT_2B):
  194. # bcrypt <1.1 lacks 2B support
  195. hash = IDENT_2A + hash[4:]
  196. elif hash.startswith(IDENT_2):
  197. # bcrypt doesn't support $2$ hashes; but we can fake it
  198. # using the $2a$ algorithm, by repeating the password until
  199. # it's 72 chars in length.
  200. hash = IDENT_2A + hash[3:]
  201. if secret:
  202. secret = repeat_string(secret, 72)
  203. elif hash.startswith(IDENT_2Y) and bcrypt.__version__ == "3.0.0":
  204. hash = IDENT_2B + hash[4:]
  205. hash = to_bytes(hash)
  206. try:
  207. return bcrypt.hashpw(secret, hash) == hash
  208. except ValueError:
  209. raise ValueError("bcrypt rejected hash: %r (secret=%r)" % (hash, secret))
  210. return check_bcrypt
  211. def fuzz_verifier_pybcrypt(self):
  212. # test against py-bcrypt, if available
  213. from passlib.handlers.bcrypt import (
  214. IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y,
  215. _PyBcryptBackend,
  216. )
  217. from passlib.utils import to_native_str
  218. loaded = _PyBcryptBackend._load_backend_mixin("pybcrypt", False)
  219. if not loaded:
  220. return
  221. from passlib.handlers.bcrypt import _pybcrypt as bcrypt_mod
  222. lock = _PyBcryptBackend._calc_lock # reuse threadlock workaround for pybcrypt 0.2
  223. def check_pybcrypt(secret, hash):
  224. """pybcrypt"""
  225. secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding)
  226. if len(secret) > 200: # vulnerable to wraparound bug
  227. secret = secret[:200]
  228. if hash.startswith((IDENT_2B, IDENT_2Y)):
  229. hash = IDENT_2A + hash[4:]
  230. try:
  231. if lock:
  232. with lock:
  233. return bcrypt_mod.hashpw(secret, hash) == hash
  234. else:
  235. return bcrypt_mod.hashpw(secret, hash) == hash
  236. except ValueError:
  237. raise ValueError("py-bcrypt rejected hash: %r" % (hash,))
  238. return check_pybcrypt
  239. def fuzz_verifier_bcryptor(self):
  240. # test against bcryptor if available
  241. from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2Y, IDENT_2B
  242. from passlib.utils import to_native_str
  243. try:
  244. from bcryptor.engine import Engine
  245. except ImportError:
  246. return
  247. def check_bcryptor(secret, hash):
  248. """bcryptor"""
  249. secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding)
  250. if hash.startswith((IDENT_2B, IDENT_2Y)):
  251. hash = IDENT_2A + hash[4:]
  252. elif hash.startswith(IDENT_2):
  253. # bcryptor doesn't support $2$ hashes; but we can fake it
  254. # using the $2a$ algorithm, by repeating the password until
  255. # it's 72 chars in length.
  256. hash = IDENT_2A + hash[3:]
  257. if secret:
  258. secret = repeat_string(secret, 72)
  259. return Engine(False).hash_key(secret, hash) == hash
  260. return check_bcryptor
  261. class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
  262. def generate(self):
  263. opts = super(_bcrypt_test.FuzzHashGenerator, self).generate()
  264. secret = opts['secret']
  265. other = opts['other']
  266. settings = opts['settings']
  267. ident = settings.get('ident')
  268. if ident == IDENT_2X:
  269. # 2x is just recognized, not supported. don't test with it.
  270. del settings['ident']
  271. elif ident == IDENT_2 and other and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret):
  272. # avoid false failure due to flaw in 0-revision bcrypt:
  273. # repeated strings like 'abc' and 'abcabc' hash identically.
  274. opts['secret'], opts['other'] = self.random_password_pair()
  275. return opts
  276. def random_rounds(self):
  277. # decrease default rounds for fuzz testing to speed up volume.
  278. return self.randintgauss(5, 8, 6, 1)
  279. #===================================================================
  280. # custom tests
  281. #===================================================================
  282. known_incorrect_padding = [
  283. # password, bad hash, good hash
  284. # 2 bits of salt padding set
  285. # ("loppux", # \/
  286. # "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C",
  287. # "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"),
  288. ("test", # \/
  289. '$2a$04$oaQbBqq8JnSM1NHRPQGXORY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO',
  290. '$2a$04$oaQbBqq8JnSM1NHRPQGXOOY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO'),
  291. # all 4 bits of salt padding set
  292. # ("Passlib11", # \/
  293. # "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK",
  294. # "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK"),
  295. ("test", # \/
  296. "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS",
  297. "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"),
  298. # bad checksum padding
  299. ("test", # \/
  300. "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV",
  301. "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"),
  302. ]
  303. def test_90_bcrypt_padding(self):
  304. """test passlib correctly handles bcrypt padding bits"""
  305. self.require_TEST_MODE("full")
  306. #
  307. # prevents reccurrence of issue 25 (https://code.google.com/p/passlib/issues/detail?id=25)
  308. # were some unused bits were incorrectly set in bcrypt salt strings.
  309. # (fixed since 1.5.3)
  310. #
  311. bcrypt = self.handler
  312. corr_desc = ".*incorrectly set padding bits"
  313. #
  314. # test hash() / genconfig() don't generate invalid salts anymore
  315. #
  316. def check_padding(hash):
  317. assert hash.startswith(("$2a$", "$2b$")) and len(hash) >= 28, \
  318. "unexpectedly malformed hash: %r" % (hash,)
  319. self.assertTrue(hash[28] in '.Oeu',
  320. "unused bits incorrectly set in hash: %r" % (hash,))
  321. for i in irange(6):
  322. check_padding(bcrypt.genconfig())
  323. for i in irange(3):
  324. check_padding(bcrypt.using(rounds=bcrypt.min_rounds).hash("bob"))
  325. #
  326. # test genconfig() corrects invalid salts & issues warning.
  327. #
  328. with self.assertWarningList(["salt too large", corr_desc]):
  329. hash = bcrypt.genconfig(salt="."*21 + "A.", rounds=5, relaxed=True)
  330. self.assertEqual(hash, "$2b$05$" + "." * (22 + 31))
  331. #
  332. # test public methods against good & bad hashes
  333. #
  334. samples = self.known_incorrect_padding
  335. for pwd, bad, good in samples:
  336. # make sure genhash() corrects bad configs, leaves good unchanged
  337. with self.assertWarningList([corr_desc]):
  338. self.assertEqual(bcrypt.genhash(pwd, bad), good)
  339. with self.assertWarningList([]):
  340. self.assertEqual(bcrypt.genhash(pwd, good), good)
  341. # make sure verify() works correctly with good & bad hashes
  342. with self.assertWarningList([corr_desc]):
  343. self.assertTrue(bcrypt.verify(pwd, bad))
  344. with self.assertWarningList([]):
  345. self.assertTrue(bcrypt.verify(pwd, good))
  346. # make sure normhash() corrects bad hashes, leaves good unchanged
  347. with self.assertWarningList([corr_desc]):
  348. self.assertEqual(bcrypt.normhash(bad), good)
  349. with self.assertWarningList([]):
  350. self.assertEqual(bcrypt.normhash(good), good)
  351. # make sure normhash() leaves non-bcrypt hashes alone
  352. self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc")
  353. def test_needs_update_w_padding(self):
  354. """needs_update corrects bcrypt padding"""
  355. # NOTE: see padding test above for details about issue this detects
  356. bcrypt = self.handler.using(rounds=4)
  357. # PASS1 = "test"
  358. # bad contains invalid 'c' char at end of salt:
  359. # \/
  360. BAD1 = "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
  361. GOOD1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
  362. self.assertTrue(bcrypt.needs_update(BAD1))
  363. self.assertFalse(bcrypt.needs_update(GOOD1))
  364. #===================================================================
  365. # eoc
  366. #===================================================================
  367. # create test cases for specific backends
  368. bcrypt_bcrypt_test = _bcrypt_test.create_backend_case("bcrypt")
  369. bcrypt_pybcrypt_test = _bcrypt_test.create_backend_case("pybcrypt")
  370. bcrypt_bcryptor_test = _bcrypt_test.create_backend_case("bcryptor")
  371. class bcrypt_os_crypt_test(_bcrypt_test.create_backend_case("os_crypt")):
  372. # os crypt doesn't support non-utf8 secret bytes
  373. known_correct_hashes = [row for row in _bcrypt_test.known_correct_hashes
  374. if is_safe_crypt_input(row[0])]
  375. # os crypt backend doesn't currently implement a per-call fallback if it fails
  376. has_os_crypt_fallback = False
  377. bcrypt_builtin_test = _bcrypt_test.create_backend_case("builtin")
  378. #=============================================================================
  379. # bcrypt
  380. #=============================================================================
  381. class _bcrypt_sha256_test(HandlerCase):
  382. "base for BCrypt-SHA256 test cases"
  383. handler = hash.bcrypt_sha256
  384. reduce_default_rounds = True
  385. forbidden_characters = None
  386. fuzz_salts_need_bcrypt_repair = True
  387. known_correct_hashes = [
  388. #-------------------------------------------------------------------
  389. # custom test vectors for old v1 format
  390. #-------------------------------------------------------------------
  391. # empty
  392. ("",
  393. '$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'),
  394. # ascii
  395. ("password",
  396. '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
  397. # unicode / utf8
  398. (UPASS_TABLE,
  399. '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
  400. (UPASS_TABLE.encode("utf-8"),
  401. '$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
  402. # ensure 2b support
  403. ("password",
  404. '$bcrypt-sha256$2b,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
  405. (UPASS_TABLE,
  406. '$bcrypt-sha256$2b,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
  407. # test >72 chars is hashed correctly -- under bcrypt these hash the same.
  408. # NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
  409. (repeat_string("abc123", 72),
  410. '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'),
  411. (repeat_string("abc123", 72) + "qwr",
  412. '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'),
  413. (repeat_string("abc123", 72) + "xyz",
  414. '$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'),
  415. #-------------------------------------------------------------------
  416. # custom test vectors for v2 format
  417. # TODO: convert to v2 format
  418. #-------------------------------------------------------------------
  419. # empty
  420. ("",
  421. '$bcrypt-sha256$v=2,t=2b,r=5$E/e/2AOhqM5W/KJTFQzLce$WFPIZKtDDTriqWwlmRFfHiOTeheAZWe'),
  422. # ascii
  423. ("password",
  424. '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'),
  425. # unicode / utf8
  426. (UPASS_TABLE,
  427. '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'),
  428. (UPASS_TABLE.encode("utf-8"),
  429. '$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'),
  430. # test >72 chars is hashed correctly -- under bcrypt these hash the same.
  431. # NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
  432. (repeat_string("abc123", 72),
  433. '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zu1cloESVFIOsUIo7fCEgkdHaI9SSue'),
  434. (repeat_string("abc123", 72) + "qwr",
  435. '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$CBF9csfEdW68xv3DwE6xSULXMtqEFP.'),
  436. (repeat_string("abc123", 72) + "xyz",
  437. '$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zC/1UDUG2ofEXB6Onr2vvyFzfhEOS3S'),
  438. ]
  439. known_correct_configs =[
  440. # v1
  441. ('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe',
  442. "password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
  443. # v2
  444. ('$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe',
  445. "password", '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'),
  446. ]
  447. known_malformed_hashes = [
  448. #-------------------------------------------------------------------
  449. # v1 format
  450. #-------------------------------------------------------------------
  451. # bad char in otherwise correct hash
  452. # \/
  453. '$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  454. # unrecognized bcrypt variant
  455. '$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  456. # unsupported bcrypt variant
  457. '$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  458. # rounds zero-padded
  459. '$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  460. # config string w/ $ added
  461. '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$',
  462. #-------------------------------------------------------------------
  463. # v2 format
  464. #-------------------------------------------------------------------
  465. # bad char in otherwise correct hash
  466. # \/
  467. '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  468. # unsupported version (for this format)
  469. '$bcrypt-sha256$v=1,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  470. # unrecognized version
  471. '$bcrypt-sha256$v=3,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  472. # unrecognized bcrypt variant
  473. '$bcrypt-sha256$v=2,t=2c,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  474. # unsupported bcrypt variant
  475. '$bcrypt-sha256$v=2,t=2a,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  476. '$bcrypt-sha256$v=2,t=2x,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  477. # rounds zero-padded
  478. '$bcrypt-sha256$v=2,t=2b,r=05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
  479. # config string w/ $ added
  480. '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$',
  481. ]
  482. #===================================================================
  483. # override some methods -- cloned from bcrypt
  484. #===================================================================
  485. def setUp(self):
  486. # ensure builtin is enabled for duration of test.
  487. if TEST_MODE("full") and self.backend == "builtin":
  488. key = "PASSLIB_BUILTIN_BCRYPT"
  489. orig = os.environ.get(key)
  490. if orig:
  491. self.addCleanup(os.environ.__setitem__, key, orig)
  492. else:
  493. self.addCleanup(os.environ.__delitem__, key)
  494. os.environ[key] = "enabled"
  495. super(_bcrypt_sha256_test, self).setUp()
  496. warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*")
  497. def populate_settings(self, kwds):
  498. # builtin is still just way too slow.
  499. if self.backend == "builtin":
  500. kwds.setdefault("rounds", 4)
  501. super(_bcrypt_sha256_test, self).populate_settings(kwds)
  502. #===================================================================
  503. # override ident tests for now
  504. #===================================================================
  505. def require_many_idents(self):
  506. raise self.skipTest("multiple idents not supported")
  507. def test_30_HasOneIdent(self):
  508. # forbidding ident keyword, we only support "2b" for now
  509. handler = self.handler
  510. handler(use_defaults=True)
  511. self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True)
  512. #===================================================================
  513. # fuzz testing -- cloned from bcrypt
  514. #===================================================================
  515. class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
  516. def random_rounds(self):
  517. # decrease default rounds for fuzz testing to speed up volume.
  518. return self.randintgauss(5, 8, 6, 1)
  519. def random_ident(self):
  520. return "2b"
  521. #===================================================================
  522. # custom tests
  523. #===================================================================
  524. def test_using_version(self):
  525. # default to v2
  526. handler = self.handler
  527. self.assertEqual(handler.version, 2)
  528. # allow v1 explicitly
  529. subcls = handler.using(version=1)
  530. self.assertEqual(subcls.version, 1)
  531. # forbid unknown ver
  532. self.assertRaises(ValueError, handler.using, version=999)
  533. # allow '2a' only for v1
  534. subcls = handler.using(version=1, ident="2a")
  535. self.assertRaises(ValueError, handler.using, ident="2a")
  536. def test_calc_digest_v2(self):
  537. """
  538. test digest calc v2 matches bcrypt()
  539. """
  540. from passlib.hash import bcrypt
  541. from passlib.crypto.digest import compile_hmac
  542. from passlib.utils.binary import b64encode
  543. # manually calc intermediary digest
  544. salt = "nyKYxTAvjmy6lMDYMl11Uu"
  545. secret = "test"
  546. temp_digest = compile_hmac("sha256", salt.encode("ascii"))(secret.encode("ascii"))
  547. temp_digest = b64encode(temp_digest).decode("ascii")
  548. self.assertEqual(temp_digest, "J5TlyIDm+IcSWmKiDJm+MeICndBkFVPn4kKdJW8f+xY=")
  549. # manually final hash from intermediary
  550. # XXX: genhash() could be useful here
  551. bcrypt_digest = bcrypt(ident="2b", salt=salt, rounds=12)._calc_checksum(temp_digest)
  552. self.assertEqual(bcrypt_digest, "M0wE0Ov/9LXoQFCe.jRHu3MSHPF54Ta")
  553. self.assertTrue(bcrypt.verify(temp_digest, "$2b$12$" + salt + bcrypt_digest))
  554. # confirm handler outputs same thing.
  555. # XXX: genhash() could be useful here
  556. result = self.handler(ident="2b", salt=salt, rounds=12)._calc_checksum(secret)
  557. self.assertEqual(result, bcrypt_digest)
  558. #===================================================================
  559. # eoc
  560. #===================================================================
  561. # create test cases for specific backends
  562. bcrypt_sha256_bcrypt_test = _bcrypt_sha256_test.create_backend_case("bcrypt")
  563. bcrypt_sha256_pybcrypt_test = _bcrypt_sha256_test.create_backend_case("pybcrypt")
  564. bcrypt_sha256_bcryptor_test = _bcrypt_sha256_test.create_backend_case("bcryptor")
  565. class bcrypt_sha256_os_crypt_test(_bcrypt_sha256_test.create_backend_case("os_crypt")):
  566. @classmethod
  567. def _get_safe_crypt_handler_backend(cls):
  568. return bcrypt_os_crypt_test._get_safe_crypt_handler_backend()
  569. has_os_crypt_fallback = False
  570. bcrypt_sha256_builtin_test = _bcrypt_sha256_test.create_backend_case("builtin")
  571. #=============================================================================
  572. # eof
  573. #=============================================================================