25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

1797 satır
73 KiB

  1. """tests for passlib.context"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. # core
  6. from __future__ import with_statement
  7. from passlib.utils.compat import PY3
  8. if PY3:
  9. from configparser import NoSectionError
  10. else:
  11. from ConfigParser import NoSectionError
  12. import datetime
  13. from functools import partial
  14. import logging; log = logging.getLogger(__name__)
  15. import os
  16. import warnings
  17. # site
  18. # pkg
  19. from passlib import hash
  20. from passlib.context import CryptContext, LazyCryptContext
  21. from passlib.exc import PasslibConfigWarning, PasslibHashWarning
  22. from passlib.utils import tick, to_unicode
  23. from passlib.utils.compat import irange, u, unicode, str_to_uascii, PY2, PY26
  24. import passlib.utils.handlers as uh
  25. from passlib.tests.utils import (TestCase, set_file, TICK_RESOLUTION,
  26. quicksleep, time_call, handler_derived_from)
  27. from passlib.registry import (register_crypt_handler_path,
  28. _has_crypt_handler as has_crypt_handler,
  29. _unload_handler_name as unload_handler_name,
  30. get_crypt_handler,
  31. )
  32. # local
  33. #=============================================================================
  34. # support
  35. #=============================================================================
  36. here = os.path.abspath(os.path.dirname(__file__))
  37. def merge_dicts(first, *args, **kwds):
  38. target = first.copy()
  39. for arg in args:
  40. target.update(arg)
  41. if kwds:
  42. target.update(kwds)
  43. return target
  44. #=============================================================================
  45. #
  46. #=============================================================================
  47. class CryptContextTest(TestCase):
  48. descriptionPrefix = "CryptContext"
  49. # TODO: these unittests could really use a good cleanup
  50. # and reorganizing, to ensure they're getting everything.
  51. #===================================================================
  52. # sample configurations used in tests
  53. #===================================================================
  54. #---------------------------------------------------------------
  55. # sample 1 - typical configuration
  56. #---------------------------------------------------------------
  57. sample_1_schemes = ["des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"]
  58. sample_1_handlers = [get_crypt_handler(name) for name in sample_1_schemes]
  59. sample_1_dict = dict(
  60. schemes = sample_1_schemes,
  61. default = "md5_crypt",
  62. all__vary_rounds = 0.1,
  63. bsdi_crypt__max_rounds = 30001,
  64. bsdi_crypt__default_rounds = 25001,
  65. sha512_crypt__max_rounds = 50000,
  66. sha512_crypt__min_rounds = 40000,
  67. )
  68. sample_1_resolved_dict = merge_dicts(sample_1_dict,
  69. schemes = sample_1_handlers)
  70. sample_1_unnormalized = u("""\
  71. [passlib]
  72. schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
  73. default = md5_crypt
  74. ; this is using %...
  75. all__vary_rounds = 10%%
  76. bsdi_crypt__default_rounds = 25001
  77. bsdi_crypt__max_rounds = 30001
  78. sha512_crypt__max_rounds = 50000
  79. sha512_crypt__min_rounds = 40000
  80. """)
  81. sample_1_unicode = u("""\
  82. [passlib]
  83. schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
  84. default = md5_crypt
  85. all__vary_rounds = 0.1
  86. bsdi_crypt__default_rounds = 25001
  87. bsdi_crypt__max_rounds = 30001
  88. sha512_crypt__max_rounds = 50000
  89. sha512_crypt__min_rounds = 40000
  90. """)
  91. #---------------------------------------------------------------
  92. # sample 1 external files
  93. #---------------------------------------------------------------
  94. # sample 1 string with '\n' linesep
  95. sample_1_path = os.path.join(here, "sample1.cfg")
  96. # sample 1 with '\r\n' linesep
  97. sample_1b_unicode = sample_1_unicode.replace(u("\n"), u("\r\n"))
  98. sample_1b_path = os.path.join(here, "sample1b.cfg")
  99. # sample 1 using UTF-16 and alt section
  100. sample_1c_bytes = sample_1_unicode.replace(u("[passlib]"),
  101. u("[mypolicy]")).encode("utf-16")
  102. sample_1c_path = os.path.join(here, "sample1c.cfg")
  103. # enable to regenerate sample files
  104. if False:
  105. set_file(sample_1_path, sample_1_unicode)
  106. set_file(sample_1b_path, sample_1b_unicode)
  107. set_file(sample_1c_path, sample_1c_bytes)
  108. #---------------------------------------------------------------
  109. # sample 2 & 12 - options patch
  110. #---------------------------------------------------------------
  111. sample_2_dict = dict(
  112. # using this to test full replacement of existing options
  113. bsdi_crypt__min_rounds = 29001,
  114. bsdi_crypt__max_rounds = 35001,
  115. bsdi_crypt__default_rounds = 31001,
  116. # using this to test partial replacement of existing options
  117. sha512_crypt__min_rounds=45000,
  118. )
  119. sample_2_unicode = """\
  120. [passlib]
  121. bsdi_crypt__min_rounds = 29001
  122. bsdi_crypt__max_rounds = 35001
  123. bsdi_crypt__default_rounds = 31001
  124. sha512_crypt__min_rounds = 45000
  125. """
  126. # sample 2 overlayed on top of sample 1
  127. sample_12_dict = merge_dicts(sample_1_dict, sample_2_dict)
  128. #---------------------------------------------------------------
  129. # sample 3 & 123 - just changing default from sample 1
  130. #---------------------------------------------------------------
  131. sample_3_dict = dict(
  132. default="sha512_crypt",
  133. )
  134. # sample 3 overlayed on 2 overlayed on 1
  135. sample_123_dict = merge_dicts(sample_12_dict, sample_3_dict)
  136. #---------------------------------------------------------------
  137. # sample 4 - used by api tests
  138. #---------------------------------------------------------------
  139. sample_4_dict = dict(
  140. schemes = [ "des_crypt", "md5_crypt", "phpass", "bsdi_crypt",
  141. "sha256_crypt"],
  142. deprecated = [ "des_crypt", ],
  143. default = "sha256_crypt",
  144. bsdi_crypt__max_rounds = 31,
  145. bsdi_crypt__default_rounds = 25,
  146. bsdi_crypt__vary_rounds = 0,
  147. sha256_crypt__max_rounds = 3000,
  148. sha256_crypt__min_rounds = 2000,
  149. sha256_crypt__default_rounds = 3000,
  150. phpass__ident = "H",
  151. phpass__default_rounds = 7,
  152. )
  153. #===================================================================
  154. # setup
  155. #===================================================================
  156. def setUp(self):
  157. super(CryptContextTest, self).setUp()
  158. warnings.filterwarnings("ignore", "The 'all' scheme is deprecated.*")
  159. warnings.filterwarnings("ignore", ".*'scheme' keyword is deprecated as of Passlib 1.7.*")
  160. #===================================================================
  161. # constructors
  162. #===================================================================
  163. def test_01_constructor(self):
  164. """test class constructor"""
  165. # test blank constructor works correctly
  166. ctx = CryptContext()
  167. self.assertEqual(ctx.to_dict(), {})
  168. # test sample 1 with scheme=names
  169. ctx = CryptContext(**self.sample_1_dict)
  170. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  171. # test sample 1 with scheme=handlers
  172. ctx = CryptContext(**self.sample_1_resolved_dict)
  173. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  174. # test sample 2: options w/o schemes
  175. ctx = CryptContext(**self.sample_2_dict)
  176. self.assertEqual(ctx.to_dict(), self.sample_2_dict)
  177. # test sample 3: default only
  178. ctx = CryptContext(**self.sample_3_dict)
  179. self.assertEqual(ctx.to_dict(), self.sample_3_dict)
  180. # test unicode scheme names (issue 54)
  181. ctx = CryptContext(schemes=[u("sha256_crypt")])
  182. self.assertEqual(ctx.schemes(), ("sha256_crypt",))
  183. def test_02_from_string(self):
  184. """test from_string() constructor"""
  185. # test sample 1 unicode
  186. ctx = CryptContext.from_string(self.sample_1_unicode)
  187. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  188. # test sample 1 with unnormalized inputs
  189. ctx = CryptContext.from_string(self.sample_1_unnormalized)
  190. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  191. # test sample 1 utf-8
  192. ctx = CryptContext.from_string(self.sample_1_unicode.encode("utf-8"))
  193. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  194. # test sample 1 w/ '\r\n' linesep
  195. ctx = CryptContext.from_string(self.sample_1b_unicode)
  196. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  197. # test sample 1 using UTF-16 and alt section
  198. ctx = CryptContext.from_string(self.sample_1c_bytes, section="mypolicy",
  199. encoding="utf-16")
  200. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  201. # test wrong type
  202. self.assertRaises(TypeError, CryptContext.from_string, None)
  203. # test missing section
  204. self.assertRaises(NoSectionError, CryptContext.from_string,
  205. self.sample_1_unicode, section="fakesection")
  206. def test_03_from_path(self):
  207. """test from_path() constructor"""
  208. # make sure sample files exist
  209. if not os.path.exists(self.sample_1_path):
  210. raise RuntimeError("can't find data file: %r" % self.sample_1_path)
  211. # test sample 1
  212. ctx = CryptContext.from_path(self.sample_1_path)
  213. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  214. # test sample 1 w/ '\r\n' linesep
  215. ctx = CryptContext.from_path(self.sample_1b_path)
  216. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  217. # test sample 1 encoding using UTF-16 and alt section
  218. ctx = CryptContext.from_path(self.sample_1c_path, section="mypolicy",
  219. encoding="utf-16")
  220. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  221. # test missing file
  222. self.assertRaises(EnvironmentError, CryptContext.from_path,
  223. os.path.join(here, "sample1xxx.cfg"))
  224. # test missing section
  225. self.assertRaises(NoSectionError, CryptContext.from_path,
  226. self.sample_1_path, section="fakesection")
  227. def test_04_copy(self):
  228. """test copy() method"""
  229. cc1 = CryptContext(**self.sample_1_dict)
  230. # overlay sample 2 onto copy
  231. cc2 = cc1.copy(**self.sample_2_dict)
  232. self.assertEqual(cc1.to_dict(), self.sample_1_dict)
  233. self.assertEqual(cc2.to_dict(), self.sample_12_dict)
  234. # check that repeating overlay makes no change
  235. cc2b = cc2.copy(**self.sample_2_dict)
  236. self.assertEqual(cc1.to_dict(), self.sample_1_dict)
  237. self.assertEqual(cc2b.to_dict(), self.sample_12_dict)
  238. # overlay sample 3 on copy
  239. cc3 = cc2.copy(**self.sample_3_dict)
  240. self.assertEqual(cc3.to_dict(), self.sample_123_dict)
  241. # test empty copy creates separate copy
  242. cc4 = cc1.copy()
  243. self.assertIsNot(cc4, cc1)
  244. self.assertEqual(cc1.to_dict(), self.sample_1_dict)
  245. self.assertEqual(cc4.to_dict(), self.sample_1_dict)
  246. # ... and that modifying copy doesn't affect original
  247. cc4.update(**self.sample_2_dict)
  248. self.assertEqual(cc1.to_dict(), self.sample_1_dict)
  249. self.assertEqual(cc4.to_dict(), self.sample_12_dict)
  250. def test_09_repr(self):
  251. """test repr()"""
  252. cc1 = CryptContext(**self.sample_1_dict)
  253. # NOTE: "0x-1234" format used by Pyston 0.5.1
  254. self.assertRegex(repr(cc1), "^<CryptContext at 0x-?[0-9a-f]+>$")
  255. #===================================================================
  256. # modifiers
  257. #===================================================================
  258. def test_10_load(self):
  259. """test load() / load_path() method"""
  260. # NOTE: load() is the workhorse that handles all policy parsing,
  261. # compilation, and validation. most of its features are tested
  262. # elsewhere, since all the constructors and modifiers are just
  263. # wrappers for it.
  264. # source_type 'auto'
  265. ctx = CryptContext()
  266. # detect dict
  267. ctx.load(self.sample_1_dict)
  268. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  269. # detect unicode string
  270. ctx.load(self.sample_1_unicode)
  271. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  272. # detect bytes string
  273. ctx.load(self.sample_1_unicode.encode("utf-8"))
  274. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  275. # anything else - TypeError
  276. self.assertRaises(TypeError, ctx.load, None)
  277. # NOTE: load_path() tested by from_path()
  278. # NOTE: additional string tests done by from_string()
  279. # update flag - tested by update() method tests
  280. # encoding keyword - tested by from_string() & from_path()
  281. # section keyword - tested by from_string() & from_path()
  282. # test load empty
  283. ctx = CryptContext(**self.sample_1_dict)
  284. ctx.load({}, update=True)
  285. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  286. # multiple loads should clear the state
  287. ctx = CryptContext()
  288. ctx.load(self.sample_1_dict)
  289. ctx.load(self.sample_2_dict)
  290. self.assertEqual(ctx.to_dict(), self.sample_2_dict)
  291. def test_11_load_rollback(self):
  292. """test load() errors restore old state"""
  293. # create initial context
  294. cc = CryptContext(["des_crypt", "sha256_crypt"],
  295. sha256_crypt__default_rounds=5000,
  296. all__vary_rounds=0.1,
  297. )
  298. result = cc.to_string()
  299. # do an update operation that should fail during parsing
  300. # XXX: not sure what the right error type is here.
  301. self.assertRaises(TypeError, cc.update, too__many__key__parts=True)
  302. self.assertEqual(cc.to_string(), result)
  303. # do an update operation that should fail during extraction
  304. # FIXME: this isn't failing even in broken case, need to figure out
  305. # way to ensure some keys come after this one.
  306. self.assertRaises(KeyError, cc.update, fake_context_option=True)
  307. self.assertEqual(cc.to_string(), result)
  308. # do an update operation that should fail during compilation
  309. self.assertRaises(ValueError, cc.update, sha256_crypt__min_rounds=10000)
  310. self.assertEqual(cc.to_string(), result)
  311. def test_12_update(self):
  312. """test update() method"""
  313. # empty overlay
  314. ctx = CryptContext(**self.sample_1_dict)
  315. ctx.update()
  316. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  317. # test basic overlay
  318. ctx = CryptContext(**self.sample_1_dict)
  319. ctx.update(**self.sample_2_dict)
  320. self.assertEqual(ctx.to_dict(), self.sample_12_dict)
  321. # ... and again
  322. ctx.update(**self.sample_3_dict)
  323. self.assertEqual(ctx.to_dict(), self.sample_123_dict)
  324. # overlay w/ dict arg
  325. ctx = CryptContext(**self.sample_1_dict)
  326. ctx.update(self.sample_2_dict)
  327. self.assertEqual(ctx.to_dict(), self.sample_12_dict)
  328. # overlay w/ string
  329. ctx = CryptContext(**self.sample_1_dict)
  330. ctx.update(self.sample_2_unicode)
  331. self.assertEqual(ctx.to_dict(), self.sample_12_dict)
  332. # too many args
  333. self.assertRaises(TypeError, ctx.update, {}, {})
  334. self.assertRaises(TypeError, ctx.update, {}, schemes=['des_crypt'])
  335. # wrong arg type
  336. self.assertRaises(TypeError, ctx.update, None)
  337. #===================================================================
  338. # option parsing
  339. #===================================================================
  340. def test_20_options(self):
  341. """test basic option parsing"""
  342. def parse(**kwds):
  343. return CryptContext(**kwds).to_dict()
  344. #
  345. # common option parsing tests
  346. #
  347. # test keys with blank fields are rejected
  348. # blank option
  349. self.assertRaises(TypeError, CryptContext, __=0.1)
  350. self.assertRaises(TypeError, CryptContext, default__scheme__='x')
  351. # blank scheme
  352. self.assertRaises(TypeError, CryptContext, __option='x')
  353. self.assertRaises(TypeError, CryptContext, default____option='x')
  354. # blank category
  355. self.assertRaises(TypeError, CryptContext, __scheme__option='x')
  356. # test keys with too many field are rejected
  357. self.assertRaises(TypeError, CryptContext,
  358. category__scheme__option__invalid = 30000)
  359. # keys with mixed separators should be handled correctly.
  360. # (testing actual data, not to_dict(), since re-render hid original bug)
  361. self.assertRaises(KeyError, parse,
  362. **{"admin.context__schemes":"md5_crypt"})
  363. ctx = CryptContext(**{"schemes":"md5_crypt,des_crypt",
  364. "admin.context__default":"des_crypt"})
  365. self.assertEqual(ctx.default_scheme("admin"), "des_crypt")
  366. #
  367. # context option -specific tests
  368. #
  369. # test context option key parsing
  370. result = dict(default="md5_crypt")
  371. self.assertEqual(parse(default="md5_crypt"), result)
  372. self.assertEqual(parse(context__default="md5_crypt"), result)
  373. self.assertEqual(parse(default__context__default="md5_crypt"), result)
  374. self.assertEqual(parse(**{"context.default":"md5_crypt"}), result)
  375. self.assertEqual(parse(**{"default.context.default":"md5_crypt"}), result)
  376. # test context option key parsing w/ category
  377. result = dict(admin__context__default="md5_crypt")
  378. self.assertEqual(parse(admin__context__default="md5_crypt"), result)
  379. self.assertEqual(parse(**{"admin.context.default":"md5_crypt"}), result)
  380. #
  381. # hash option -specific tests
  382. #
  383. # test hash option key parsing
  384. result = dict(all__vary_rounds=0.1)
  385. self.assertEqual(parse(all__vary_rounds=0.1), result)
  386. self.assertEqual(parse(default__all__vary_rounds=0.1), result)
  387. self.assertEqual(parse(**{"all.vary_rounds":0.1}), result)
  388. self.assertEqual(parse(**{"default.all.vary_rounds":0.1}), result)
  389. # test hash option key parsing w/ category
  390. result = dict(admin__all__vary_rounds=0.1)
  391. self.assertEqual(parse(admin__all__vary_rounds=0.1), result)
  392. self.assertEqual(parse(**{"admin.all.vary_rounds":0.1}), result)
  393. # settings not allowed if not in hash.setting_kwds
  394. ctx = CryptContext(["phpass", "md5_crypt"], phpass__ident="P")
  395. self.assertRaises(KeyError, ctx.copy, md5_crypt__ident="P")
  396. # hash options 'salt' and 'rounds' not allowed
  397. self.assertRaises(KeyError, CryptContext, schemes=["des_crypt"],
  398. des_crypt__salt="xx")
  399. self.assertRaises(KeyError, CryptContext, schemes=["des_crypt"],
  400. all__salt="xx")
  401. def test_21_schemes(self):
  402. """test 'schemes' context option parsing"""
  403. # schemes can be empty
  404. cc = CryptContext(schemes=None)
  405. self.assertEqual(cc.schemes(), ())
  406. # schemes can be list of names
  407. cc = CryptContext(schemes=["des_crypt", "md5_crypt"])
  408. self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt"))
  409. # schemes can be comma-sep string
  410. cc = CryptContext(schemes=" des_crypt, md5_crypt, ")
  411. self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt"))
  412. # schemes can be list of handlers
  413. cc = CryptContext(schemes=[hash.des_crypt, hash.md5_crypt])
  414. self.assertEqual(cc.schemes(), ("des_crypt", "md5_crypt"))
  415. # scheme must be name or handler
  416. self.assertRaises(TypeError, CryptContext, schemes=[uh.StaticHandler])
  417. # handlers must have a name
  418. class nameless(uh.StaticHandler):
  419. name = None
  420. self.assertRaises(ValueError, CryptContext, schemes=[nameless])
  421. # names must be unique
  422. class dummy_1(uh.StaticHandler):
  423. name = 'dummy_1'
  424. self.assertRaises(KeyError, CryptContext, schemes=[dummy_1, dummy_1])
  425. # schemes not allowed per-category
  426. self.assertRaises(KeyError, CryptContext,
  427. admin__context__schemes=["md5_crypt"])
  428. def test_22_deprecated(self):
  429. """test 'deprecated' context option parsing"""
  430. def getdep(ctx, category=None):
  431. return [name for name in ctx.schemes()
  432. if ctx.handler(name, category).deprecated]
  433. # no schemes - all deprecated values allowed
  434. cc = CryptContext(deprecated=["md5_crypt"])
  435. cc.update(schemes=["md5_crypt", "des_crypt"])
  436. self.assertEqual(getdep(cc),["md5_crypt"])
  437. # deprecated values allowed if subset of schemes
  438. cc = CryptContext(deprecated=["md5_crypt"], schemes=["md5_crypt", "des_crypt"])
  439. self.assertEqual(getdep(cc), ["md5_crypt"])
  440. # can be handler
  441. # XXX: allow handlers in deprecated list? not for now.
  442. self.assertRaises(TypeError, CryptContext, deprecated=[hash.md5_crypt],
  443. schemes=["md5_crypt", "des_crypt"])
  444. ## cc = CryptContext(deprecated=[hash.md5_crypt], schemes=["md5_crypt", "des_crypt"])
  445. ## self.assertEqual(getdep(cc), ["md5_crypt"])
  446. # comma sep list
  447. cc = CryptContext(deprecated="md5_crypt,des_crypt", schemes=["md5_crypt", "des_crypt", "sha256_crypt"])
  448. self.assertEqual(getdep(cc), ["md5_crypt", "des_crypt"])
  449. # values outside of schemes not allowed
  450. self.assertRaises(KeyError, CryptContext, schemes=['des_crypt'],
  451. deprecated=['md5_crypt'])
  452. # deprecating ALL schemes should cause ValueError
  453. self.assertRaises(ValueError, CryptContext,
  454. schemes=['des_crypt'],
  455. deprecated=['des_crypt'])
  456. self.assertRaises(ValueError, CryptContext,
  457. schemes=['des_crypt', 'md5_crypt'],
  458. admin__context__deprecated=['des_crypt', 'md5_crypt'])
  459. # deprecating explicit default scheme should cause ValueError
  460. # ... default listed as deprecated
  461. self.assertRaises(ValueError, CryptContext,
  462. schemes=['des_crypt', 'md5_crypt'],
  463. default="md5_crypt",
  464. deprecated="md5_crypt")
  465. # ... global default deprecated per-category
  466. self.assertRaises(ValueError, CryptContext,
  467. schemes=['des_crypt', 'md5_crypt'],
  468. default="md5_crypt",
  469. admin__context__deprecated="md5_crypt")
  470. # ... category default deprecated globally
  471. self.assertRaises(ValueError, CryptContext,
  472. schemes=['des_crypt', 'md5_crypt'],
  473. admin__context__default="md5_crypt",
  474. deprecated="md5_crypt")
  475. # ... category default deprecated in category
  476. self.assertRaises(ValueError, CryptContext,
  477. schemes=['des_crypt', 'md5_crypt'],
  478. admin__context__default="md5_crypt",
  479. admin__context__deprecated="md5_crypt")
  480. # category deplist should shadow default deplist
  481. CryptContext(
  482. schemes=['des_crypt', 'md5_crypt'],
  483. deprecated="md5_crypt",
  484. admin__context__default="md5_crypt",
  485. admin__context__deprecated=[])
  486. # wrong type
  487. self.assertRaises(TypeError, CryptContext, deprecated=123)
  488. # deprecated per-category
  489. cc = CryptContext(deprecated=["md5_crypt"],
  490. schemes=["md5_crypt", "des_crypt"],
  491. admin__context__deprecated=["des_crypt"],
  492. )
  493. self.assertEqual(getdep(cc), ["md5_crypt"])
  494. self.assertEqual(getdep(cc, "user"), ["md5_crypt"])
  495. self.assertEqual(getdep(cc, "admin"), ["des_crypt"])
  496. # blank per-category deprecated list, shadowing default list
  497. cc = CryptContext(deprecated=["md5_crypt"],
  498. schemes=["md5_crypt", "des_crypt"],
  499. admin__context__deprecated=[],
  500. )
  501. self.assertEqual(getdep(cc), ["md5_crypt"])
  502. self.assertEqual(getdep(cc, "user"), ["md5_crypt"])
  503. self.assertEqual(getdep(cc, "admin"), [])
  504. def test_23_default(self):
  505. """test 'default' context option parsing"""
  506. # anything allowed if no schemes
  507. self.assertEqual(CryptContext(default="md5_crypt").to_dict(),
  508. dict(default="md5_crypt"))
  509. # default allowed if in scheme list
  510. ctx = CryptContext(default="md5_crypt", schemes=["des_crypt", "md5_crypt"])
  511. self.assertEqual(ctx.default_scheme(), "md5_crypt")
  512. # default can be handler
  513. # XXX: sure we want to allow this ? maybe deprecate in future.
  514. ctx = CryptContext(default=hash.md5_crypt, schemes=["des_crypt", "md5_crypt"])
  515. self.assertEqual(ctx.default_scheme(), "md5_crypt")
  516. # implicit default should be first non-deprecated scheme
  517. ctx = CryptContext(schemes=["des_crypt", "md5_crypt"])
  518. self.assertEqual(ctx.default_scheme(), "des_crypt")
  519. ctx.update(deprecated="des_crypt")
  520. self.assertEqual(ctx.default_scheme(), "md5_crypt")
  521. # error if not in scheme list
  522. self.assertRaises(KeyError, CryptContext, schemes=['des_crypt'],
  523. default='md5_crypt')
  524. # wrong type
  525. self.assertRaises(TypeError, CryptContext, default=1)
  526. # per-category
  527. ctx = CryptContext(default="des_crypt",
  528. schemes=["des_crypt", "md5_crypt"],
  529. admin__context__default="md5_crypt")
  530. self.assertEqual(ctx.default_scheme(), "des_crypt")
  531. self.assertEqual(ctx.default_scheme("user"), "des_crypt")
  532. self.assertEqual(ctx.default_scheme("admin"), "md5_crypt")
  533. def test_24_vary_rounds(self):
  534. """test 'vary_rounds' hash option parsing"""
  535. def parse(v):
  536. return CryptContext(all__vary_rounds=v).to_dict()['all__vary_rounds']
  537. # floats should be preserved
  538. self.assertEqual(parse(0.1), 0.1)
  539. self.assertEqual(parse('0.1'), 0.1)
  540. # 'xx%' should be converted to float
  541. self.assertEqual(parse('10%'), 0.1)
  542. # ints should be preserved
  543. self.assertEqual(parse(1000), 1000)
  544. self.assertEqual(parse('1000'), 1000)
  545. #===================================================================
  546. # inspection & serialization
  547. #===================================================================
  548. def assertHandlerDerivedFrom(self, handler, base, msg=None):
  549. self.assertTrue(handler_derived_from(handler, base), msg=msg)
  550. def test_30_schemes(self):
  551. """test schemes() method"""
  552. # NOTE: also checked under test_21
  553. # test empty
  554. ctx = CryptContext()
  555. self.assertEqual(ctx.schemes(), ())
  556. self.assertEqual(ctx.schemes(resolve=True), ())
  557. # test sample 1
  558. ctx = CryptContext(**self.sample_1_dict)
  559. self.assertEqual(ctx.schemes(), tuple(self.sample_1_schemes))
  560. self.assertEqual(ctx.schemes(resolve=True, unconfigured=True), tuple(self.sample_1_handlers))
  561. for result, correct in zip(ctx.schemes(resolve=True), self.sample_1_handlers):
  562. self.assertTrue(handler_derived_from(result, correct))
  563. # test sample 2
  564. ctx = CryptContext(**self.sample_2_dict)
  565. self.assertEqual(ctx.schemes(), ())
  566. def test_31_default_scheme(self):
  567. """test default_scheme() method"""
  568. # NOTE: also checked under test_23
  569. # test empty
  570. ctx = CryptContext()
  571. self.assertRaises(KeyError, ctx.default_scheme)
  572. # test sample 1
  573. ctx = CryptContext(**self.sample_1_dict)
  574. self.assertEqual(ctx.default_scheme(), "md5_crypt")
  575. self.assertEqual(ctx.default_scheme(resolve=True, unconfigured=True), hash.md5_crypt)
  576. self.assertHandlerDerivedFrom(ctx.default_scheme(resolve=True), hash.md5_crypt)
  577. # test sample 2
  578. ctx = CryptContext(**self.sample_2_dict)
  579. self.assertRaises(KeyError, ctx.default_scheme)
  580. # test defaults to first in scheme
  581. ctx = CryptContext(schemes=self.sample_1_schemes)
  582. self.assertEqual(ctx.default_scheme(), "des_crypt")
  583. # categories tested under test_23
  584. def test_32_handler(self):
  585. """test handler() method"""
  586. # default for empty
  587. ctx = CryptContext()
  588. self.assertRaises(KeyError, ctx.handler)
  589. self.assertRaises(KeyError, ctx.handler, "md5_crypt")
  590. # default for sample 1
  591. ctx = CryptContext(**self.sample_1_dict)
  592. self.assertEqual(ctx.handler(unconfigured=True), hash.md5_crypt)
  593. self.assertHandlerDerivedFrom(ctx.handler(), hash.md5_crypt)
  594. # by name
  595. self.assertEqual(ctx.handler("des_crypt", unconfigured=True), hash.des_crypt)
  596. self.assertHandlerDerivedFrom(ctx.handler("des_crypt"), hash.des_crypt)
  597. # name not in schemes
  598. self.assertRaises(KeyError, ctx.handler, "mysql323")
  599. # check handler() honors category default
  600. ctx = CryptContext("sha256_crypt,md5_crypt", admin__context__default="md5_crypt")
  601. self.assertEqual(ctx.handler(unconfigured=True), hash.sha256_crypt)
  602. self.assertHandlerDerivedFrom(ctx.handler(), hash.sha256_crypt)
  603. self.assertEqual(ctx.handler(category="staff", unconfigured=True), hash.sha256_crypt)
  604. self.assertHandlerDerivedFrom(ctx.handler(category="staff"), hash.sha256_crypt)
  605. self.assertEqual(ctx.handler(category="admin", unconfigured=True), hash.md5_crypt)
  606. self.assertHandlerDerivedFrom(ctx.handler(category="staff"), hash.sha256_crypt)
  607. # test unicode category strings are accepted under py2
  608. if PY2:
  609. self.assertEqual(ctx.handler(category=u("staff"), unconfigured=True), hash.sha256_crypt)
  610. self.assertEqual(ctx.handler(category=u("admin"), unconfigured=True), hash.md5_crypt)
  611. def test_33_options(self):
  612. """test internal _get_record_options() method"""
  613. def options(ctx, scheme, category=None):
  614. return ctx._config._get_record_options_with_flag(scheme, category)[0]
  615. # this checks that (3 schemes, 3 categories) inherit options correctly.
  616. # the 'user' category is not present in the options.
  617. cc4 = CryptContext(
  618. truncate_error=True,
  619. schemes = [ "sha512_crypt", "des_crypt", "bsdi_crypt"],
  620. deprecated = ["sha512_crypt", "des_crypt"],
  621. all__vary_rounds = 0.1,
  622. bsdi_crypt__vary_rounds=0.2,
  623. sha512_crypt__max_rounds = 20000,
  624. admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ],
  625. admin__all__vary_rounds = 0.05,
  626. admin__bsdi_crypt__vary_rounds=0.3,
  627. admin__sha512_crypt__max_rounds = 40000,
  628. )
  629. self.assertEqual(cc4._config.categories, ("admin",))
  630. #
  631. # sha512_crypt
  632. # NOTE: 'truncate_error' shouldn't be passed along...
  633. #
  634. self.assertEqual(options(cc4, "sha512_crypt"), dict(
  635. deprecated=True,
  636. vary_rounds=0.1, # inherited from all__
  637. max_rounds=20000,
  638. ))
  639. self.assertEqual(options(cc4, "sha512_crypt", "user"), dict(
  640. deprecated=True, # unconfigured category inherits from default
  641. vary_rounds=0.1,
  642. max_rounds=20000,
  643. ))
  644. self.assertEqual(options(cc4, "sha512_crypt", "admin"), dict(
  645. # NOT deprecated - context option overridden per-category
  646. vary_rounds=0.05, # global overridden per-cateogry
  647. max_rounds=40000, # overridden per-category
  648. ))
  649. #
  650. # des_crypt
  651. # NOTE: vary_rounds shouldn't be passed along...
  652. #
  653. self.assertEqual(options(cc4, "des_crypt"), dict(
  654. deprecated=True,
  655. truncate_error=True,
  656. ))
  657. self.assertEqual(options(cc4, "des_crypt", "user"), dict(
  658. deprecated=True, # unconfigured category inherits from default
  659. truncate_error=True,
  660. ))
  661. self.assertEqual(options(cc4, "des_crypt", "admin"), dict(
  662. deprecated=True, # unchanged though overidden
  663. truncate_error=True,
  664. ))
  665. #
  666. # bsdi_crypt
  667. #
  668. self.assertEqual(options(cc4, "bsdi_crypt"), dict(
  669. vary_rounds=0.2, # overridden from all__vary_rounds
  670. ))
  671. self.assertEqual(options(cc4, "bsdi_crypt", "user"), dict(
  672. vary_rounds=0.2, # unconfigured category inherits from default
  673. ))
  674. self.assertEqual(options(cc4, "bsdi_crypt", "admin"), dict(
  675. vary_rounds=0.3,
  676. deprecated=True, # deprecation set per-category
  677. ))
  678. def test_34_to_dict(self):
  679. """test to_dict() method"""
  680. # NOTE: this is tested all throughout this test case.
  681. ctx = CryptContext(**self.sample_1_dict)
  682. self.assertEqual(ctx.to_dict(), self.sample_1_dict)
  683. self.assertEqual(ctx.to_dict(resolve=True), self.sample_1_resolved_dict)
  684. def test_35_to_string(self):
  685. """test to_string() method"""
  686. # create ctx and serialize
  687. ctx = CryptContext(**self.sample_1_dict)
  688. dump = ctx.to_string()
  689. # check ctx->string returns canonical format.
  690. # NOTE: ConfigParser for PY26 doesn't use OrderedDict,
  691. # making to_string()'s ordering unpredictable...
  692. # so we skip this test under PY26.
  693. if not PY26:
  694. self.assertEqual(dump, self.sample_1_unicode)
  695. # check ctx->string->ctx->dict returns original
  696. ctx2 = CryptContext.from_string(dump)
  697. self.assertEqual(ctx2.to_dict(), self.sample_1_dict)
  698. # test section kwd is honored
  699. other = ctx.to_string(section="password-security")
  700. self.assertEqual(other, dump.replace("[passlib]","[password-security]"))
  701. # test unmanaged handler warning
  702. from passlib.tests.test_utils_handlers import UnsaltedHash
  703. ctx3 = CryptContext([UnsaltedHash, "md5_crypt"])
  704. dump = ctx3.to_string()
  705. self.assertRegex(dump, r"# NOTE: the 'unsalted_test_hash' handler\(s\)"
  706. r" are not registered with Passlib")
  707. #===================================================================
  708. # password hash api
  709. #===================================================================
  710. nonstring_vectors = [
  711. (None, {}),
  712. (None, {"scheme": "des_crypt"}),
  713. (1, {}),
  714. ((), {}),
  715. ]
  716. def test_40_basic(self):
  717. """test basic hash/identify/verify functionality"""
  718. handlers = [hash.md5_crypt, hash.des_crypt, hash.bsdi_crypt]
  719. cc = CryptContext(handlers, bsdi_crypt__default_rounds=5)
  720. # run through handlers
  721. for crypt in handlers:
  722. h = cc.hash("test", scheme=crypt.name)
  723. self.assertEqual(cc.identify(h), crypt.name)
  724. self.assertEqual(cc.identify(h, resolve=True, unconfigured=True), crypt)
  725. self.assertHandlerDerivedFrom(cc.identify(h, resolve=True), crypt)
  726. self.assertTrue(cc.verify('test', h))
  727. self.assertFalse(cc.verify('notest', h))
  728. # test default
  729. h = cc.hash("test")
  730. self.assertEqual(cc.identify(h), "md5_crypt")
  731. # test genhash
  732. h = cc.genhash('secret', cc.genconfig())
  733. self.assertEqual(cc.identify(h), 'md5_crypt')
  734. h = cc.genhash('secret', cc.genconfig(), scheme='md5_crypt')
  735. self.assertEqual(cc.identify(h), 'md5_crypt')
  736. self.assertRaises(ValueError, cc.genhash, 'secret', cc.genconfig(), scheme="des_crypt")
  737. def test_41_genconfig(self):
  738. """test genconfig() method"""
  739. cc = CryptContext(schemes=["md5_crypt", "phpass"],
  740. phpass__ident="H",
  741. phpass__default_rounds=7,
  742. admin__phpass__ident="P",
  743. )
  744. # uses default scheme
  745. self.assertTrue(cc.genconfig().startswith("$1$"))
  746. # override scheme
  747. self.assertTrue(cc.genconfig(scheme="phpass").startswith("$H$5"))
  748. # category override
  749. self.assertTrue(cc.genconfig(scheme="phpass", category="admin").startswith("$P$5"))
  750. self.assertTrue(cc.genconfig(scheme="phpass", category="staff").startswith("$H$5"))
  751. # override scheme & custom settings
  752. self.assertEqual(
  753. cc.genconfig(scheme="phpass", salt='.'*8, rounds=8, ident='P'),
  754. '$P$6........22zGEuacuPOqEpYPDeR0R/', # NOTE: config string generated w/ rounds=1
  755. )
  756. #--------------------------------------------------------------
  757. # border cases
  758. #--------------------------------------------------------------
  759. # test unicode category strings are accepted under py2
  760. # this tests basic _get_record() used by hash/genhash/verify.
  761. # we have to omit scheme=xxx so codepath is tested fully
  762. if PY2:
  763. c2 = cc.copy(default="phpass")
  764. self.assertTrue(c2.genconfig(category=u("admin")).startswith("$P$5"))
  765. self.assertTrue(c2.genconfig(category=u("staff")).startswith("$H$5"))
  766. # throws error without schemes
  767. self.assertRaises(KeyError, CryptContext().genconfig)
  768. self.assertRaises(KeyError, CryptContext().genconfig, scheme='md5_crypt')
  769. # bad scheme values
  770. self.assertRaises(KeyError, cc.genconfig, scheme="fake") # XXX: should this be ValueError?
  771. self.assertRaises(TypeError, cc.genconfig, scheme=1, category='staff')
  772. self.assertRaises(TypeError, cc.genconfig, scheme=1)
  773. # bad category values
  774. self.assertRaises(TypeError, cc.genconfig, category=1)
  775. def test_42_genhash(self):
  776. """test genhash() method"""
  777. #--------------------------------------------------------------
  778. # border cases
  779. #--------------------------------------------------------------
  780. # rejects non-string secrets
  781. cc = CryptContext(["des_crypt"])
  782. hash = cc.hash('stub')
  783. for secret, kwds in self.nonstring_vectors:
  784. self.assertRaises(TypeError, cc.genhash, secret, hash, **kwds)
  785. # rejects non-string config strings
  786. cc = CryptContext(["des_crypt"])
  787. for config, kwds in self.nonstring_vectors:
  788. if hash is None:
  789. # NOTE: as of 1.7, genhash is just wrapper for hash(),
  790. # and handles genhash(secret, None) fine.
  791. continue
  792. self.assertRaises(TypeError, cc.genhash, 'secret', config, **kwds)
  793. # rejects config=None, even if default scheme lacks config string
  794. cc = CryptContext(["mysql323"])
  795. self.assertRaises(TypeError, cc.genhash, "stub", None)
  796. # throws error without schemes
  797. self.assertRaises(KeyError, CryptContext().genhash, 'secret', 'hash')
  798. # bad scheme values
  799. self.assertRaises(KeyError, cc.genhash, 'secret', hash, scheme="fake") # XXX: should this be ValueError?
  800. self.assertRaises(TypeError, cc.genhash, 'secret', hash, scheme=1)
  801. # bad category values
  802. self.assertRaises(TypeError, cc.genconfig, 'secret', hash, category=1)
  803. def test_43_hash(self,):
  804. """test hash() method"""
  805. # XXX: what more can we test here that isn't deprecated
  806. # or handled under another test (e.g. context kwds?)
  807. # respects rounds
  808. cc = CryptContext(**self.sample_4_dict)
  809. hash = cc.hash("password")
  810. self.assertTrue(hash.startswith("$5$rounds=3000$"))
  811. self.assertTrue(cc.verify("password", hash))
  812. self.assertFalse(cc.verify("passwordx", hash))
  813. # make default > max throws error if attempted
  814. # XXX: move this to copy() test?
  815. self.assertRaises(ValueError, cc.copy,
  816. sha256_crypt__default_rounds=4000)
  817. # rejects non-string secrets
  818. cc = CryptContext(["des_crypt"])
  819. for secret, kwds in self.nonstring_vectors:
  820. self.assertRaises(TypeError, cc.hash, secret, **kwds)
  821. # throws error without schemes
  822. self.assertRaises(KeyError, CryptContext().hash, 'secret')
  823. # bad category values
  824. self.assertRaises(TypeError, cc.hash, 'secret', category=1)
  825. def test_43_hash_legacy(self, use_16_legacy=False):
  826. """test hash() method -- legacy 'scheme' and settings keywords"""
  827. cc = CryptContext(**self.sample_4_dict)
  828. # TODO: should migrate these tests elsewhere, or remove them.
  829. # can be replaced with following equivalent:
  830. #
  831. # def wrapper(secret, scheme=None, category=None, **kwds):
  832. # handler = cc.handler(scheme, category)
  833. # if kwds:
  834. # handler = handler.using(**kwds)
  835. # return handler.hash(secret)
  836. #
  837. # need to make sure bits being tested here are tested
  838. # under the tests for the equivalent methods called above,
  839. # and then discard the rest of these under 2.0.
  840. # hash specific settings
  841. with self.assertWarningList(["passing settings to.*is deprecated"]):
  842. self.assertEqual(
  843. cc.hash("password", scheme="phpass", salt='.'*8),
  844. '$H$5........De04R5Egz0aq8Tf.1eVhY/',
  845. )
  846. with self.assertWarningList(["passing settings to.*is deprecated"]):
  847. self.assertEqual(
  848. cc.hash("password", scheme="phpass", salt='.'*8, ident="P"),
  849. '$P$5........De04R5Egz0aq8Tf.1eVhY/',
  850. )
  851. # NOTE: more thorough job of rounds limits done below.
  852. # min rounds
  853. with self.assertWarningList(["passing settings to.*is deprecated"]):
  854. self.assertEqual(
  855. cc.hash("password", rounds=1999, salt="nacl"),
  856. '$5$rounds=1999$nacl$nmfwJIxqj0csloAAvSER0B8LU0ERCAbhmMug4Twl609',
  857. )
  858. with self.assertWarningList(["passing settings to.*is deprecated"]):
  859. self.assertEqual(
  860. cc.hash("password", rounds=2001, salt="nacl"),
  861. '$5$rounds=2001$nacl$8PdeoPL4aXQnJ0woHhqgIw/efyfCKC2WHneOpnvF.31'
  862. )
  863. # NOTE: max rounds, etc tested in genconfig()
  864. # bad scheme values
  865. self.assertRaises(KeyError, cc.hash, 'secret', scheme="fake") # XXX: should this be ValueError?
  866. self.assertRaises(TypeError, cc.hash, 'secret', scheme=1)
  867. def test_44_identify(self):
  868. """test identify() border cases"""
  869. handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"]
  870. cc = CryptContext(handlers, bsdi_crypt__default_rounds=5)
  871. # check unknown hash
  872. self.assertEqual(cc.identify('$9$232323123$1287319827'), None)
  873. self.assertRaises(ValueError, cc.identify, '$9$232323123$1287319827', required=True)
  874. #--------------------------------------------------------------
  875. # border cases
  876. #--------------------------------------------------------------
  877. # rejects non-string hashes
  878. cc = CryptContext(["des_crypt"])
  879. for hash, kwds in self.nonstring_vectors:
  880. self.assertRaises(TypeError, cc.identify, hash, **kwds)
  881. # throws error without schemes
  882. cc = CryptContext()
  883. self.assertIs(cc.identify('hash'), None)
  884. self.assertRaises(KeyError, cc.identify, 'hash', required=True)
  885. # bad category values
  886. self.assertRaises(TypeError, cc.identify, None, category=1)
  887. def test_45_verify(self):
  888. """test verify() scheme kwd"""
  889. handlers = ["md5_crypt", "des_crypt", "bsdi_crypt"]
  890. cc = CryptContext(handlers, bsdi_crypt__default_rounds=5)
  891. h = hash.md5_crypt.hash("test")
  892. # check base verify
  893. self.assertTrue(cc.verify("test", h))
  894. self.assertTrue(not cc.verify("notest", h))
  895. # check verify using right alg
  896. self.assertTrue(cc.verify('test', h, scheme='md5_crypt'))
  897. self.assertTrue(not cc.verify('notest', h, scheme='md5_crypt'))
  898. # check verify using wrong alg
  899. self.assertRaises(ValueError, cc.verify, 'test', h, scheme='bsdi_crypt')
  900. #--------------------------------------------------------------
  901. # border cases
  902. #--------------------------------------------------------------
  903. # unknown hash should throw error
  904. self.assertRaises(ValueError, cc.verify, 'stub', '$6$232323123$1287319827')
  905. # rejects non-string secrets
  906. cc = CryptContext(["des_crypt"])
  907. h = refhash = cc.hash('stub')
  908. for secret, kwds in self.nonstring_vectors:
  909. self.assertRaises(TypeError, cc.verify, secret, h, **kwds)
  910. # always treat hash=None as False
  911. self.assertFalse(cc.verify(secret, None))
  912. # rejects non-string hashes
  913. cc = CryptContext(["des_crypt"])
  914. for h, kwds in self.nonstring_vectors:
  915. if h is None:
  916. continue
  917. self.assertRaises(TypeError, cc.verify, 'secret', h, **kwds)
  918. # throws error without schemes
  919. self.assertRaises(KeyError, CryptContext().verify, 'secret', 'hash')
  920. # bad scheme values
  921. self.assertRaises(KeyError, cc.verify, 'secret', refhash, scheme="fake") # XXX: should this be ValueError?
  922. self.assertRaises(TypeError, cc.verify, 'secret', refhash, scheme=1)
  923. # bad category values
  924. self.assertRaises(TypeError, cc.verify, 'secret', refhash, category=1)
  925. def test_46_needs_update(self):
  926. """test needs_update() method"""
  927. cc = CryptContext(**self.sample_4_dict)
  928. # check deprecated scheme
  929. self.assertTrue(cc.needs_update('9XXD4trGYeGJA'))
  930. self.assertFalse(cc.needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0'))
  931. # check min rounds
  932. self.assertTrue(cc.needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/'))
  933. self.assertFalse(cc.needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8'))
  934. # check max rounds
  935. self.assertFalse(cc.needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.'))
  936. self.assertTrue(cc.needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA'))
  937. #--------------------------------------------------------------
  938. # test hash.needs_update() interface
  939. #--------------------------------------------------------------
  940. check_state = []
  941. class dummy(uh.StaticHandler):
  942. name = 'dummy'
  943. _hash_prefix = '@'
  944. @classmethod
  945. def needs_update(cls, hash, secret=None):
  946. check_state.append((hash, secret))
  947. return secret == "nu"
  948. def _calc_checksum(self, secret):
  949. from hashlib import md5
  950. if isinstance(secret, unicode):
  951. secret = secret.encode("utf-8")
  952. return str_to_uascii(md5(secret).hexdigest())
  953. # calling needs_update should query callback
  954. ctx = CryptContext([dummy])
  955. hash = refhash = dummy.hash("test")
  956. self.assertFalse(ctx.needs_update(hash))
  957. self.assertEqual(check_state, [(hash,None)])
  958. del check_state[:]
  959. # now with a password
  960. self.assertFalse(ctx.needs_update(hash, secret='bob'))
  961. self.assertEqual(check_state, [(hash,'bob')])
  962. del check_state[:]
  963. # now when it returns True
  964. self.assertTrue(ctx.needs_update(hash, secret='nu'))
  965. self.assertEqual(check_state, [(hash,'nu')])
  966. del check_state[:]
  967. #--------------------------------------------------------------
  968. # border cases
  969. #--------------------------------------------------------------
  970. # rejects non-string hashes
  971. cc = CryptContext(["des_crypt"])
  972. for hash, kwds in self.nonstring_vectors:
  973. self.assertRaises(TypeError, cc.needs_update, hash, **kwds)
  974. # throws error without schemes
  975. self.assertRaises(KeyError, CryptContext().needs_update, 'hash')
  976. # bad scheme values
  977. self.assertRaises(KeyError, cc.needs_update, refhash, scheme="fake") # XXX: should this be ValueError?
  978. self.assertRaises(TypeError, cc.needs_update, refhash, scheme=1)
  979. # bad category values
  980. self.assertRaises(TypeError, cc.needs_update, refhash, category=1)
  981. def test_47_verify_and_update(self):
  982. """test verify_and_update()"""
  983. cc = CryptContext(**self.sample_4_dict)
  984. # create some hashes
  985. h1 = cc.handler("des_crypt").hash("password")
  986. h2 = cc.handler("sha256_crypt").hash("password")
  987. # check bad password, deprecated hash
  988. ok, new_hash = cc.verify_and_update("wrongpass", h1)
  989. self.assertFalse(ok)
  990. self.assertIs(new_hash, None)
  991. # check bad password, good hash
  992. ok, new_hash = cc.verify_and_update("wrongpass", h2)
  993. self.assertFalse(ok)
  994. self.assertIs(new_hash, None)
  995. # check right password, deprecated hash
  996. ok, new_hash = cc.verify_and_update("password", h1)
  997. self.assertTrue(ok)
  998. self.assertTrue(cc.identify(new_hash), "sha256_crypt")
  999. # check right password, good hash
  1000. ok, new_hash = cc.verify_and_update("password", h2)
  1001. self.assertTrue(ok)
  1002. self.assertIs(new_hash, None)
  1003. #--------------------------------------------------------------
  1004. # border cases
  1005. #--------------------------------------------------------------
  1006. # rejects non-string secrets
  1007. cc = CryptContext(["des_crypt"])
  1008. hash = refhash = cc.hash('stub')
  1009. for secret, kwds in self.nonstring_vectors:
  1010. self.assertRaises(TypeError, cc.verify_and_update, secret, hash, **kwds)
  1011. # always treat hash=None as False
  1012. self.assertEqual(cc.verify_and_update(secret, None), (False, None))
  1013. # rejects non-string hashes
  1014. cc = CryptContext(["des_crypt"])
  1015. for hash, kwds in self.nonstring_vectors:
  1016. if hash is None:
  1017. continue
  1018. self.assertRaises(TypeError, cc.verify_and_update, 'secret', hash, **kwds)
  1019. # throws error without schemes
  1020. self.assertRaises(KeyError, CryptContext().verify_and_update, 'secret', 'hash')
  1021. # bad scheme values
  1022. self.assertRaises(KeyError, cc.verify_and_update, 'secret', refhash, scheme="fake") # XXX: should this be ValueError?
  1023. self.assertRaises(TypeError, cc.verify_and_update, 'secret', refhash, scheme=1)
  1024. # bad category values
  1025. self.assertRaises(TypeError, cc.verify_and_update, 'secret', refhash, category=1)
  1026. def test_48_context_kwds(self):
  1027. """hash(), verify(), and verify_and_update() -- discard unused context keywords"""
  1028. # setup test case
  1029. # NOTE: postgres_md5 hash supports 'user' context kwd, which is used for this test.
  1030. from passlib.hash import des_crypt, md5_crypt, postgres_md5
  1031. des_hash = des_crypt.hash("stub")
  1032. pg_root_hash = postgres_md5.hash("stub", user="root")
  1033. pg_admin_hash = postgres_md5.hash("stub", user="admin")
  1034. #------------------------------------------------------------
  1035. # case 1: contextual kwds not supported by any hash in CryptContext
  1036. #------------------------------------------------------------
  1037. cc1 = CryptContext([des_crypt, md5_crypt])
  1038. self.assertEqual(cc1.context_kwds, set())
  1039. # des_scrypt should work w/o any contextual kwds
  1040. self.assertTrue(des_crypt.identify(cc1.hash("stub")), "des_crypt")
  1041. self.assertTrue(cc1.verify("stub", des_hash))
  1042. self.assertEqual(cc1.verify_and_update("stub", des_hash), (True, None))
  1043. # des_crypt should throw error due to unknown context keyword
  1044. with self.assertWarningList(["passing settings to.*is deprecated"]):
  1045. self.assertRaises(TypeError, cc1.hash, "stub", user="root")
  1046. self.assertRaises(TypeError, cc1.verify, "stub", des_hash, user="root")
  1047. self.assertRaises(TypeError, cc1.verify_and_update, "stub", des_hash, user="root")
  1048. #------------------------------------------------------------
  1049. # case 2: at least one contextual kwd supported by non-default hash
  1050. #------------------------------------------------------------
  1051. cc2 = CryptContext([des_crypt, postgres_md5])
  1052. self.assertEqual(cc2.context_kwds, set(["user"]))
  1053. # verify des_crypt works w/o "user" kwd
  1054. self.assertTrue(des_crypt.identify(cc2.hash("stub")), "des_crypt")
  1055. self.assertTrue(cc2.verify("stub", des_hash))
  1056. self.assertEqual(cc2.verify_and_update("stub", des_hash), (True, None))
  1057. # verify des_crypt ignores "user" kwd
  1058. self.assertTrue(des_crypt.identify(cc2.hash("stub", user="root")), "des_crypt")
  1059. self.assertTrue(cc2.verify("stub", des_hash, user="root"))
  1060. self.assertEqual(cc2.verify_and_update("stub", des_hash, user="root"), (True, None))
  1061. # verify error with unknown kwd
  1062. with self.assertWarningList(["passing settings to.*is deprecated"]):
  1063. self.assertRaises(TypeError, cc2.hash, "stub", badkwd="root")
  1064. self.assertRaises(TypeError, cc2.verify, "stub", des_hash, badkwd="root")
  1065. self.assertRaises(TypeError, cc2.verify_and_update, "stub", des_hash, badkwd="root")
  1066. #------------------------------------------------------------
  1067. # case 3: at least one contextual kwd supported by default hash
  1068. #------------------------------------------------------------
  1069. cc3 = CryptContext([postgres_md5, des_crypt], deprecated="auto")
  1070. self.assertEqual(cc3.context_kwds, set(["user"]))
  1071. # postgres_md5 should have error w/o context kwd
  1072. self.assertRaises(TypeError, cc3.hash, "stub")
  1073. self.assertRaises(TypeError, cc3.verify, "stub", pg_root_hash)
  1074. self.assertRaises(TypeError, cc3.verify_and_update, "stub", pg_root_hash)
  1075. # postgres_md5 should work w/ context kwd
  1076. self.assertEqual(cc3.hash("stub", user="root"), pg_root_hash)
  1077. self.assertTrue(cc3.verify("stub", pg_root_hash, user="root"))
  1078. self.assertEqual(cc3.verify_and_update("stub", pg_root_hash, user="root"), (True, None))
  1079. # verify_and_update() should fail against wrong user
  1080. self.assertEqual(cc3.verify_and_update("stub", pg_root_hash, user="admin"), (False, None))
  1081. # verify_and_update() should pass all context kwds through when rehashing
  1082. self.assertEqual(cc3.verify_and_update("stub", des_hash, user="root"),
  1083. (True, pg_root_hash))
  1084. #===================================================================
  1085. # rounds options
  1086. #===================================================================
  1087. # TODO: now that rounds generation has moved out of _CryptRecord to HasRounds,
  1088. # this should just test that we're passing right options to handler.using(),
  1089. # and that resulting handler has right settings.
  1090. # Can then just let HasRounds tests (which are a copy of this) deal with things.
  1091. # NOTE: the follow tests check how _CryptRecord handles
  1092. # the min/max/default/vary_rounds options, via the output of
  1093. # genconfig(). it's assumed hash() takes the same codepath.
  1094. def test_50_rounds_limits(self):
  1095. """test rounds limits"""
  1096. cc = CryptContext(schemes=["sha256_crypt"],
  1097. sha256_crypt__min_rounds=2000,
  1098. sha256_crypt__max_rounds=3000,
  1099. sha256_crypt__default_rounds=2500,
  1100. )
  1101. # stub digest returned by sha256_crypt's genconfig calls..
  1102. STUB = '...........................................'
  1103. #--------------------------------------------------
  1104. # settings should have been applied to custom handler,
  1105. # it should take care of the rest
  1106. #--------------------------------------------------
  1107. custom_handler = cc._get_record("sha256_crypt", None)
  1108. self.assertEqual(custom_handler.min_desired_rounds, 2000)
  1109. self.assertEqual(custom_handler.max_desired_rounds, 3000)
  1110. self.assertEqual(custom_handler.default_rounds, 2500)
  1111. #--------------------------------------------------
  1112. # min_rounds
  1113. #--------------------------------------------------
  1114. # set below handler minimum
  1115. with self.assertWarningList([PasslibHashWarning]*2):
  1116. c2 = cc.copy(sha256_crypt__min_rounds=500, sha256_crypt__max_rounds=None,
  1117. sha256_crypt__default_rounds=500)
  1118. self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=1000$nacl$" + STUB)
  1119. # below policy minimum
  1120. # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .replace()
  1121. with self.assertWarningList([]):
  1122. self.assertEqual(
  1123. cc.genconfig(rounds=1999, salt="nacl"), '$5$rounds=1999$nacl$' + STUB)
  1124. # equal to policy minimum
  1125. self.assertEqual(
  1126. cc.genconfig(rounds=2000, salt="nacl"), '$5$rounds=2000$nacl$' + STUB)
  1127. # above policy minimum
  1128. self.assertEqual(
  1129. cc.genconfig(rounds=2001, salt="nacl"), '$5$rounds=2001$nacl$' + STUB)
  1130. #--------------------------------------------------
  1131. # max rounds
  1132. #--------------------------------------------------
  1133. # set above handler max
  1134. with self.assertWarningList([PasslibHashWarning]*2):
  1135. c2 = cc.copy(sha256_crypt__max_rounds=int(1e9)+500, sha256_crypt__min_rounds=None,
  1136. sha256_crypt__default_rounds=int(1e9)+500)
  1137. self.assertEqual(c2.genconfig(salt="nacl"), "$5$rounds=999999999$nacl$" + STUB)
  1138. # above policy max
  1139. # NOTE: formerly issued a warning in passlib 1.6, now just a wrapper for .using()
  1140. with self.assertWarningList([]):
  1141. self.assertEqual(
  1142. cc.genconfig(rounds=3001, salt="nacl"), '$5$rounds=3001$nacl$' + STUB)
  1143. # equal policy max
  1144. self.assertEqual(
  1145. cc.genconfig(rounds=3000, salt="nacl"), '$5$rounds=3000$nacl$' + STUB)
  1146. # below policy max
  1147. self.assertEqual(
  1148. cc.genconfig(rounds=2999, salt="nacl"), '$5$rounds=2999$nacl$' + STUB)
  1149. #--------------------------------------------------
  1150. # default_rounds
  1151. #--------------------------------------------------
  1152. # explicit default rounds
  1153. self.assertEqual(cc.genconfig(salt="nacl"), '$5$rounds=2500$nacl$' + STUB)
  1154. # fallback default rounds - use handler's
  1155. df = hash.sha256_crypt.default_rounds
  1156. c2 = cc.copy(sha256_crypt__default_rounds=None, sha256_crypt__max_rounds=df<<1)
  1157. self.assertEqual(c2.genconfig(salt="nacl"), '$5$rounds=%d$nacl$%s' % (df, STUB))
  1158. # fallback default rounds - use handler's, but clipped to max rounds
  1159. c2 = cc.copy(sha256_crypt__default_rounds=None, sha256_crypt__max_rounds=3000)
  1160. self.assertEqual(c2.genconfig(salt="nacl"), '$5$rounds=3000$nacl$' + STUB)
  1161. # TODO: test default falls back to mx / mn if handler has no default.
  1162. # default rounds - out of bounds
  1163. self.assertRaises(ValueError, cc.copy, sha256_crypt__default_rounds=1999)
  1164. cc.copy(sha256_crypt__default_rounds=2000)
  1165. cc.copy(sha256_crypt__default_rounds=3000)
  1166. self.assertRaises(ValueError, cc.copy, sha256_crypt__default_rounds=3001)
  1167. #--------------------------------------------------
  1168. # border cases
  1169. #--------------------------------------------------
  1170. # invalid min/max bounds
  1171. c2 = CryptContext(schemes=["sha256_crypt"])
  1172. # NOTE: as of v1.7, these are clipped w/ a warning instead...
  1173. # self.assertRaises(ValueError, c2.copy, sha256_crypt__min_rounds=-1)
  1174. # self.assertRaises(ValueError, c2.copy, sha256_crypt__max_rounds=-1)
  1175. self.assertRaises(ValueError, c2.copy, sha256_crypt__min_rounds=2000,
  1176. sha256_crypt__max_rounds=1999)
  1177. # test bad values
  1178. self.assertRaises(ValueError, CryptContext, sha256_crypt__min_rounds='x')
  1179. self.assertRaises(ValueError, CryptContext, sha256_crypt__max_rounds='x')
  1180. self.assertRaises(ValueError, CryptContext, all__vary_rounds='x')
  1181. self.assertRaises(ValueError, CryptContext, sha256_crypt__default_rounds='x')
  1182. # test bad types rejected
  1183. bad = datetime.datetime.now() # picked cause can't be compared to int
  1184. self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__min_rounds=bad)
  1185. self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__max_rounds=bad)
  1186. self.assertRaises(TypeError, CryptContext, "sha256_crypt", all__vary_rounds=bad)
  1187. self.assertRaises(TypeError, CryptContext, "sha256_crypt", sha256_crypt__default_rounds=bad)
  1188. def test_51_linear_vary_rounds(self):
  1189. """test linear vary rounds"""
  1190. cc = CryptContext(schemes=["sha256_crypt"],
  1191. sha256_crypt__min_rounds=1995,
  1192. sha256_crypt__max_rounds=2005,
  1193. sha256_crypt__default_rounds=2000,
  1194. )
  1195. # test negative
  1196. self.assertRaises(ValueError, cc.copy, all__vary_rounds=-1)
  1197. self.assertRaises(ValueError, cc.copy, all__vary_rounds="-1%")
  1198. self.assertRaises(ValueError, cc.copy, all__vary_rounds="101%")
  1199. # test static
  1200. c2 = cc.copy(all__vary_rounds=0)
  1201. self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0)
  1202. self.assert_rounds_range(c2, "sha256_crypt", 2000, 2000)
  1203. c2 = cc.copy(all__vary_rounds="0%")
  1204. self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0)
  1205. self.assert_rounds_range(c2, "sha256_crypt", 2000, 2000)
  1206. # test absolute
  1207. c2 = cc.copy(all__vary_rounds=1)
  1208. self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 1)
  1209. self.assert_rounds_range(c2, "sha256_crypt", 1999, 2001)
  1210. c2 = cc.copy(all__vary_rounds=100)
  1211. self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 100)
  1212. self.assert_rounds_range(c2, "sha256_crypt", 1995, 2005)
  1213. # test relative
  1214. c2 = cc.copy(all__vary_rounds="0.1%")
  1215. self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 0.001)
  1216. self.assert_rounds_range(c2, "sha256_crypt", 1998, 2002)
  1217. c2 = cc.copy(all__vary_rounds="100%")
  1218. self.assertEqual(c2._get_record("sha256_crypt", None).vary_rounds, 1.0)
  1219. self.assert_rounds_range(c2, "sha256_crypt", 1995, 2005)
  1220. def test_52_log2_vary_rounds(self):
  1221. """test log2 vary rounds"""
  1222. cc = CryptContext(schemes=["bcrypt"],
  1223. bcrypt__min_rounds=15,
  1224. bcrypt__max_rounds=25,
  1225. bcrypt__default_rounds=20,
  1226. )
  1227. # test negative
  1228. self.assertRaises(ValueError, cc.copy, all__vary_rounds=-1)
  1229. self.assertRaises(ValueError, cc.copy, all__vary_rounds="-1%")
  1230. self.assertRaises(ValueError, cc.copy, all__vary_rounds="101%")
  1231. # test static
  1232. c2 = cc.copy(all__vary_rounds=0)
  1233. self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0)
  1234. self.assert_rounds_range(c2, "bcrypt", 20, 20)
  1235. c2 = cc.copy(all__vary_rounds="0%")
  1236. self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0)
  1237. self.assert_rounds_range(c2, "bcrypt", 20, 20)
  1238. # test absolute
  1239. c2 = cc.copy(all__vary_rounds=1)
  1240. self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 1)
  1241. self.assert_rounds_range(c2, "bcrypt", 19, 21)
  1242. c2 = cc.copy(all__vary_rounds=100)
  1243. self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 100)
  1244. self.assert_rounds_range(c2, "bcrypt", 15, 25)
  1245. # test relative - should shift over at 50% mark
  1246. c2 = cc.copy(all__vary_rounds="1%")
  1247. self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.01)
  1248. self.assert_rounds_range(c2, "bcrypt", 20, 20)
  1249. c2 = cc.copy(all__vary_rounds="49%")
  1250. self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.49)
  1251. self.assert_rounds_range(c2, "bcrypt", 20, 20)
  1252. c2 = cc.copy(all__vary_rounds="50%")
  1253. self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 0.5)
  1254. self.assert_rounds_range(c2, "bcrypt", 19, 20)
  1255. c2 = cc.copy(all__vary_rounds="100%")
  1256. self.assertEqual(c2._get_record("bcrypt", None).vary_rounds, 1.0)
  1257. self.assert_rounds_range(c2, "bcrypt", 15, 21)
  1258. def assert_rounds_range(self, context, scheme, lower, upper):
  1259. """helper to check vary_rounds covers specified range"""
  1260. # NOTE: this runs enough times the min and max *should* be hit,
  1261. # though there's a faint chance it will randomly fail.
  1262. handler = context.handler(scheme)
  1263. salt = handler.default_salt_chars[0:1] * handler.max_salt_size
  1264. seen = set()
  1265. for i in irange(300):
  1266. h = context.genconfig(scheme, salt=salt)
  1267. r = handler.from_string(h).rounds
  1268. seen.add(r)
  1269. self.assertEqual(min(seen), lower, "vary_rounds had wrong lower limit:")
  1270. self.assertEqual(max(seen), upper, "vary_rounds had wrong upper limit:")
  1271. #===================================================================
  1272. # harden_verify / min_verify_time
  1273. #===================================================================
  1274. def test_harden_verify_parsing(self):
  1275. """harden_verify -- parsing"""
  1276. warnings.filterwarnings("ignore", ".*harden_verify.*",
  1277. category=DeprecationWarning)
  1278. # valid values
  1279. ctx = CryptContext(schemes=["sha256_crypt"])
  1280. self.assertEqual(ctx.harden_verify, None)
  1281. self.assertEqual(ctx.using(harden_verify="").harden_verify, None)
  1282. self.assertEqual(ctx.using(harden_verify="true").harden_verify, None)
  1283. self.assertEqual(ctx.using(harden_verify="false").harden_verify, None)
  1284. def test_dummy_verify(self):
  1285. """
  1286. dummy_verify() method
  1287. """
  1288. # check dummy_verify() takes expected time
  1289. expected = 0.05
  1290. accuracy = 0.2
  1291. handler = DelayHash.using()
  1292. handler.delay = expected
  1293. ctx = CryptContext(schemes=[handler])
  1294. ctx.dummy_verify() # prime the memoized helpers
  1295. elapsed, _ = time_call(ctx.dummy_verify)
  1296. self.assertAlmostEqual(elapsed, expected, delta=expected * accuracy)
  1297. # TODO: test dummy_verify() invoked by .verify() when hash is None,
  1298. # and same for .verify_and_update()
  1299. #===================================================================
  1300. # feature tests
  1301. #===================================================================
  1302. def test_61_autodeprecate(self):
  1303. """test deprecated='auto' is handled correctly"""
  1304. def getstate(ctx, category=None):
  1305. return [ctx.handler(scheme, category).deprecated for scheme in ctx.schemes()]
  1306. # correctly reports default
  1307. ctx = CryptContext("sha256_crypt,md5_crypt,des_crypt", deprecated="auto")
  1308. self.assertEqual(getstate(ctx, None), [False, True, True])
  1309. self.assertEqual(getstate(ctx, "admin"), [False, True, True])
  1310. # correctly reports changed default
  1311. ctx.update(default="md5_crypt")
  1312. self.assertEqual(getstate(ctx, None), [True, False, True])
  1313. self.assertEqual(getstate(ctx, "admin"), [True, False, True])
  1314. # category default is handled correctly
  1315. ctx.update(admin__context__default="des_crypt")
  1316. self.assertEqual(getstate(ctx, None), [True, False, True])
  1317. self.assertEqual(getstate(ctx, "admin"), [True, True, False])
  1318. # handles 1 scheme
  1319. ctx = CryptContext(["sha256_crypt"], deprecated="auto")
  1320. self.assertEqual(getstate(ctx, None), [False])
  1321. self.assertEqual(getstate(ctx, "admin"), [False])
  1322. # disallow auto & other deprecated schemes at same time.
  1323. self.assertRaises(ValueError, CryptContext, "sha256_crypt,md5_crypt",
  1324. deprecated="auto,md5_crypt")
  1325. self.assertRaises(ValueError, CryptContext, "sha256_crypt,md5_crypt",
  1326. deprecated="md5_crypt,auto")
  1327. def test_disabled_hashes(self):
  1328. """disabled hash support"""
  1329. #
  1330. # init ref info
  1331. #
  1332. from passlib.hash import md5_crypt, unix_disabled
  1333. ctx = CryptContext(["des_crypt"])
  1334. ctx2 = CryptContext(["des_crypt", "unix_disabled"])
  1335. h_ref = ctx.hash("foo")
  1336. h_other = md5_crypt.hash('foo')
  1337. #
  1338. # ctx.disable()
  1339. #
  1340. # test w/o disabled hash support
  1341. self.assertRaisesRegex(RuntimeError, "no disabled hasher present",
  1342. ctx.disable)
  1343. self.assertRaisesRegex(RuntimeError, "no disabled hasher present",
  1344. ctx.disable, h_ref)
  1345. self.assertRaisesRegex(RuntimeError, "no disabled hasher present",
  1346. ctx.disable, h_other)
  1347. # test w/ disabled hash support
  1348. h_dis = ctx2.disable()
  1349. self.assertEqual(h_dis, unix_disabled.default_marker)
  1350. h_dis_ref = ctx2.disable(h_ref)
  1351. self.assertEqual(h_dis_ref, unix_disabled.default_marker + h_ref)
  1352. h_dis_other = ctx2.disable(h_other)
  1353. self.assertEqual(h_dis_other, unix_disabled.default_marker + h_other)
  1354. # don't double-wrap existing disabled hash
  1355. self.assertEqual(ctx2.disable(h_dis_ref), h_dis_ref)
  1356. #
  1357. # ctx.is_enabled()
  1358. #
  1359. # test w/o disabled hash support
  1360. self.assertTrue(ctx.is_enabled(h_ref))
  1361. HASH_NOT_IDENTIFIED = "hash could not be identified"
  1362. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1363. ctx.is_enabled, h_other)
  1364. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1365. ctx.is_enabled, h_dis)
  1366. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1367. ctx.is_enabled, h_dis_ref)
  1368. # test w/ disabled hash support
  1369. self.assertTrue(ctx2.is_enabled(h_ref))
  1370. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1371. ctx.is_enabled, h_other)
  1372. self.assertFalse(ctx2.is_enabled(h_dis))
  1373. self.assertFalse(ctx2.is_enabled(h_dis_ref))
  1374. #
  1375. # ctx.enable()
  1376. #
  1377. # test w/o disabled hash support
  1378. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1379. ctx.enable, "")
  1380. self.assertRaises(TypeError, ctx.enable, None)
  1381. self.assertEqual(ctx.enable(h_ref), h_ref)
  1382. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1383. ctx.enable, h_other)
  1384. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1385. ctx.enable, h_dis)
  1386. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1387. ctx.enable, h_dis_ref)
  1388. # test w/ disabled hash support
  1389. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1390. ctx.enable, "")
  1391. self.assertRaises(TypeError, ctx2.enable, None)
  1392. self.assertEqual(ctx2.enable(h_ref), h_ref)
  1393. self.assertRaisesRegex(ValueError, HASH_NOT_IDENTIFIED,
  1394. ctx2.enable, h_other)
  1395. self.assertRaisesRegex(ValueError, "cannot restore original hash",
  1396. ctx2.enable, h_dis)
  1397. self.assertEqual(ctx2.enable(h_dis_ref), h_ref)
  1398. #===================================================================
  1399. # eoc
  1400. #===================================================================
  1401. import hashlib, time
  1402. class DelayHash(uh.StaticHandler):
  1403. """dummy hasher which delays by specified amount"""
  1404. name = "delay_hash"
  1405. checksum_chars = uh.LOWER_HEX_CHARS
  1406. checksum_size = 40
  1407. delay = 0
  1408. _hash_prefix = u("$x$")
  1409. def _calc_checksum(self, secret):
  1410. time.sleep(self.delay)
  1411. if isinstance(secret, unicode):
  1412. secret = secret.encode("utf-8")
  1413. return str_to_uascii(hashlib.sha1(b"prefix" + secret).hexdigest())
  1414. #=============================================================================
  1415. # LazyCryptContext
  1416. #=============================================================================
  1417. class dummy_2(uh.StaticHandler):
  1418. name = "dummy_2"
  1419. class LazyCryptContextTest(TestCase):
  1420. descriptionPrefix = "LazyCryptContext"
  1421. def setUp(self):
  1422. # make sure this isn't registered before OR after
  1423. unload_handler_name("dummy_2")
  1424. self.addCleanup(unload_handler_name, "dummy_2")
  1425. def test_kwd_constructor(self):
  1426. """test plain kwds"""
  1427. self.assertFalse(has_crypt_handler("dummy_2"))
  1428. register_crypt_handler_path("dummy_2", "passlib.tests.test_context")
  1429. cc = LazyCryptContext(iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"])
  1430. self.assertFalse(has_crypt_handler("dummy_2", True))
  1431. self.assertEqual(cc.schemes(), ("dummy_2", "des_crypt"))
  1432. self.assertTrue(cc.handler("des_crypt").deprecated)
  1433. self.assertTrue(has_crypt_handler("dummy_2", True))
  1434. def test_callable_constructor(self):
  1435. self.assertFalse(has_crypt_handler("dummy_2"))
  1436. register_crypt_handler_path("dummy_2", "passlib.tests.test_context")
  1437. def onload(flag=False):
  1438. self.assertTrue(flag)
  1439. return dict(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"])
  1440. cc = LazyCryptContext(onload=onload, flag=True)
  1441. self.assertFalse(has_crypt_handler("dummy_2", True))
  1442. self.assertEqual(cc.schemes(), ("dummy_2", "des_crypt"))
  1443. self.assertTrue(cc.handler("des_crypt").deprecated)
  1444. self.assertTrue(has_crypt_handler("dummy_2", True))
  1445. #=============================================================================
  1446. # eof
  1447. #=============================================================================