test_crypto_digest.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. """tests for passlib.utils.(des|pbkdf2|md4)"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. from __future__ import with_statement, division
  6. # core
  7. from binascii import hexlify
  8. import hashlib
  9. import warnings
  10. # site
  11. # pkg
  12. # module
  13. from passlib.exc import UnknownHashError
  14. from passlib.utils.compat import PY3, u, JYTHON
  15. from passlib.tests.utils import TestCase, TEST_MODE, skipUnless, hb
  16. #=============================================================================
  17. # test assorted crypto helpers
  18. #=============================================================================
  19. class HashInfoTest(TestCase):
  20. """test various crypto functions"""
  21. descriptionPrefix = "passlib.crypto.digest"
  22. #: list of formats norm_hash_name() should support
  23. norm_hash_formats = ["hashlib", "iana"]
  24. #: test cases for norm_hash_name()
  25. #: each row contains (iana name, hashlib name, ... 0+ unnormalized names)
  26. norm_hash_samples = [
  27. # real hashes
  28. ("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"),
  29. ("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"),
  30. ("sha256", "sha-256", "SHA_256", "sha2-256"),
  31. ("ripemd160", "ripemd-160", "SCRAM-RIPEMD-160", "RIPEmd160",
  32. # NOTE: there was an older "RIPEMD" & "RIPEMD-128", but python treates "RIPEMD"
  33. # as alias for "RIPEMD-160"
  34. "ripemd", "SCRAM-RIPEMD"),
  35. # fake hashes (to check if fallback normalization behaves sanely)
  36. ("sha4_256", "sha4-256", "SHA4-256", "SHA-4-256"),
  37. ("test128", "test-128", "TEST128"),
  38. ("test2", "test2", "TEST-2"),
  39. ("test3_128", "test3-128", "TEST-3-128"),
  40. ]
  41. def test_norm_hash_name(self):
  42. """norm_hash_name()"""
  43. from itertools import chain
  44. from passlib.crypto.digest import norm_hash_name, _known_hash_names
  45. # snapshot warning state, ignore unknown hash warnings
  46. ctx = warnings.catch_warnings()
  47. ctx.__enter__()
  48. self.addCleanup(ctx.__exit__)
  49. warnings.filterwarnings("ignore", '.*unknown hash')
  50. warnings.filterwarnings("ignore", '.*unsupported hash')
  51. # test string types
  52. self.assertEqual(norm_hash_name(u("MD4")), "md4")
  53. self.assertEqual(norm_hash_name(b"MD4"), "md4")
  54. self.assertRaises(TypeError, norm_hash_name, None)
  55. # test selected results
  56. for row in chain(_known_hash_names, self.norm_hash_samples):
  57. for idx, format in enumerate(self.norm_hash_formats):
  58. correct = row[idx]
  59. for value in row:
  60. result = norm_hash_name(value, format)
  61. self.assertEqual(result, correct,
  62. "name=%r, format=%r:" % (value,
  63. format))
  64. def test_lookup_hash_ctor(self):
  65. """lookup_hash() -- constructor"""
  66. from passlib.crypto.digest import lookup_hash
  67. # invalid/unknown names should be rejected
  68. self.assertRaises(ValueError, lookup_hash, "new")
  69. self.assertRaises(ValueError, lookup_hash, "__name__")
  70. self.assertRaises(ValueError, lookup_hash, "sha4")
  71. # 1. should return hashlib builtin if found
  72. self.assertEqual(lookup_hash("md5"), (hashlib.md5, 16, 64))
  73. # 2. should return wrapper around hashlib.new() if found
  74. try:
  75. hashlib.new("sha")
  76. has_sha = True
  77. except ValueError:
  78. has_sha = False
  79. if has_sha:
  80. record = lookup_hash("sha")
  81. const = record[0]
  82. self.assertEqual(record, (const, 20, 64))
  83. self.assertEqual(hexlify(const(b"abc").digest()),
  84. b"0164b8a914cd2a5e74c4f7ff082c4d97f1edf880")
  85. else:
  86. self.assertRaises(ValueError, lookup_hash, "sha")
  87. # 3. should fall back to builtin md4
  88. try:
  89. hashlib.new("md4")
  90. has_md4 = True
  91. except ValueError:
  92. has_md4 = False
  93. record = lookup_hash("md4")
  94. const = record[0]
  95. if not has_md4:
  96. from passlib.crypto._md4 import md4
  97. self.assertIs(const, md4)
  98. self.assertEqual(record, (const, 16, 64))
  99. self.assertEqual(hexlify(const(b"abc").digest()),
  100. b"a448017aaf21d8525fc10ae87aa6729d")
  101. # should memoize records
  102. self.assertIs(lookup_hash("md5"), lookup_hash("md5"))
  103. def test_lookup_hash_w_unknown_name(self):
  104. """lookup_hash() -- unknown hash name"""
  105. from passlib.crypto.digest import lookup_hash
  106. # unknown names should be rejected by default
  107. self.assertRaises(UnknownHashError, lookup_hash, "xxx256")
  108. # required=False should return stub record instead
  109. info = lookup_hash("xxx256", required=False)
  110. self.assertFalse(info.supported)
  111. self.assertRaisesRegex(UnknownHashError, "unknown hash: 'xxx256'", info.const)
  112. self.assertEqual(info.name, "xxx256")
  113. self.assertEqual(info.digest_size, None)
  114. self.assertEqual(info.block_size, None)
  115. # should cache stub records
  116. info2 = lookup_hash("xxx256", required=False)
  117. self.assertIs(info2, info)
  118. def test_mock_fips_mode(self):
  119. """
  120. lookup_hash() -- test set_mock_fips_mode()
  121. """
  122. from passlib.crypto.digest import lookup_hash, _set_mock_fips_mode
  123. # check if md5 is available so we can test mock helper
  124. if not lookup_hash("md5", required=False).supported:
  125. raise self.skipTest("md5 not supported")
  126. # enable monkeypatch to mock up fips mode
  127. _set_mock_fips_mode()
  128. self.addCleanup(_set_mock_fips_mode, False)
  129. pat = "'md5' hash disabled for fips"
  130. self.assertRaisesRegex(UnknownHashError, pat, lookup_hash, "md5")
  131. info = lookup_hash("md5", required=False)
  132. self.assertRegex(info.error_text, pat)
  133. self.assertRaisesRegex(UnknownHashError, pat, info.const)
  134. # should use hardcoded fallback info
  135. self.assertEqual(info.digest_size, 16)
  136. self.assertEqual(info.block_size, 64)
  137. def test_lookup_hash_metadata(self):
  138. """lookup_hash() -- metadata"""
  139. from passlib.crypto.digest import lookup_hash
  140. # quick test of metadata using known reference - sha256
  141. info = lookup_hash("sha256")
  142. self.assertEqual(info.name, "sha256")
  143. self.assertEqual(info.iana_name, "sha-256")
  144. self.assertEqual(info.block_size, 64)
  145. self.assertEqual(info.digest_size, 32)
  146. self.assertIs(lookup_hash("SHA2-256"), info)
  147. # quick test of metadata using known reference - md5
  148. info = lookup_hash("md5")
  149. self.assertEqual(info.name, "md5")
  150. self.assertEqual(info.iana_name, "md5")
  151. self.assertEqual(info.block_size, 64)
  152. self.assertEqual(info.digest_size, 16)
  153. def test_lookup_hash_alt_types(self):
  154. """lookup_hash() -- alternate types"""
  155. from passlib.crypto.digest import lookup_hash
  156. info = lookup_hash("sha256")
  157. self.assertIs(lookup_hash(info), info)
  158. self.assertIs(lookup_hash(info.const), info)
  159. self.assertRaises(TypeError, lookup_hash, 123)
  160. # TODO: write full test of compile_hmac() -- currently relying on pbkdf2_hmac() tests
  161. #=============================================================================
  162. # test PBKDF1 support
  163. #=============================================================================
  164. class Pbkdf1_Test(TestCase):
  165. """test kdf helpers"""
  166. descriptionPrefix = "passlib.crypto.digest.pbkdf1"
  167. pbkdf1_tests = [
  168. # (password, salt, rounds, keylen, hash, result)
  169. #
  170. # from http://www.di-mgt.com.au/cryptoKDFs.html
  171. #
  172. (b'password', hb('78578E5A5D63CB06'), 1000, 16, 'sha1', hb('dc19847e05c64d2faf10ebfb4a3d2a20')),
  173. #
  174. # custom
  175. #
  176. (b'password', b'salt', 1000, 0, 'md5', b''),
  177. (b'password', b'salt', 1000, 1, 'md5', hb('84')),
  178. (b'password', b'salt', 1000, 8, 'md5', hb('8475c6a8531a5d27')),
  179. (b'password', b'salt', 1000, 16, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
  180. (b'password', b'salt', 1000, None, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
  181. (b'password', b'salt', 1000, None, 'sha1', hb('4a8fd48e426ed081b535be5769892fa396293efb')),
  182. ]
  183. if not JYTHON: # FIXME: find out why not jython, or reenable this.
  184. pbkdf1_tests.append(
  185. (b'password', b'salt', 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453'))
  186. )
  187. def test_known(self):
  188. """test reference vectors"""
  189. from passlib.crypto.digest import pbkdf1
  190. for secret, salt, rounds, keylen, digest, correct in self.pbkdf1_tests:
  191. result = pbkdf1(digest, secret, salt, rounds, keylen)
  192. self.assertEqual(result, correct)
  193. def test_border(self):
  194. """test border cases"""
  195. from passlib.crypto.digest import pbkdf1
  196. def helper(secret=b'secret', salt=b'salt', rounds=1, keylen=1, hash='md5'):
  197. return pbkdf1(hash, secret, salt, rounds, keylen)
  198. helper()
  199. # salt/secret wrong type
  200. self.assertRaises(TypeError, helper, secret=1)
  201. self.assertRaises(TypeError, helper, salt=1)
  202. # non-existent hashes
  203. self.assertRaises(ValueError, helper, hash='missing')
  204. # rounds < 1 and wrong type
  205. self.assertRaises(ValueError, helper, rounds=0)
  206. self.assertRaises(TypeError, helper, rounds='1')
  207. # keylen < 0, keylen > block_size, and wrong type
  208. self.assertRaises(ValueError, helper, keylen=-1)
  209. self.assertRaises(ValueError, helper, keylen=17, hash='md5')
  210. self.assertRaises(TypeError, helper, keylen='1')
  211. #=============================================================================
  212. # test PBKDF2-HMAC support
  213. #=============================================================================
  214. # import the test subject
  215. from passlib.crypto.digest import pbkdf2_hmac, PBKDF2_BACKENDS
  216. # NOTE: relying on tox to verify this works under all the various backends.
  217. class Pbkdf2Test(TestCase):
  218. """test pbkdf2() support"""
  219. descriptionPrefix = "passlib.crypto.digest.pbkdf2_hmac() <backends: %s>" % ", ".join(PBKDF2_BACKENDS)
  220. pbkdf2_test_vectors = [
  221. # (result, secret, salt, rounds, keylen, digest="sha1")
  222. #
  223. # from rfc 3962
  224. #
  225. # test case 1 / 128 bit
  226. (
  227. hb("cdedb5281bb2f801565a1122b2563515"),
  228. b"password", b"ATHENA.MIT.EDUraeburn", 1, 16
  229. ),
  230. # test case 2 / 128 bit
  231. (
  232. hb("01dbee7f4a9e243e988b62c73cda935d"),
  233. b"password", b"ATHENA.MIT.EDUraeburn", 2, 16
  234. ),
  235. # test case 2 / 256 bit
  236. (
  237. hb("01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86"),
  238. b"password", b"ATHENA.MIT.EDUraeburn", 2, 32
  239. ),
  240. # test case 3 / 256 bit
  241. (
  242. hb("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13"),
  243. b"password", b"ATHENA.MIT.EDUraeburn", 1200, 32
  244. ),
  245. # test case 4 / 256 bit
  246. (
  247. hb("d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee"),
  248. b"password", b'\x12\x34\x56\x78\x78\x56\x34\x12', 5, 32
  249. ),
  250. # test case 5 / 256 bit
  251. (
  252. hb("139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1"),
  253. b"X"*64, b"pass phrase equals block size", 1200, 32
  254. ),
  255. # test case 6 / 256 bit
  256. (
  257. hb("9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a"),
  258. b"X"*65, b"pass phrase exceeds block size", 1200, 32
  259. ),
  260. #
  261. # from rfc 6070
  262. #
  263. (
  264. hb("0c60c80f961f0e71f3a9b524af6012062fe037a6"),
  265. b"password", b"salt", 1, 20,
  266. ),
  267. (
  268. hb("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"),
  269. b"password", b"salt", 2, 20,
  270. ),
  271. (
  272. hb("4b007901b765489abead49d926f721d065a429c1"),
  273. b"password", b"salt", 4096, 20,
  274. ),
  275. # just runs too long - could enable if ALL option is set
  276. ##(
  277. ##
  278. ## hb("eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"),
  279. ## "password", "salt", 16777216, 20,
  280. ##),
  281. (
  282. hb("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"),
  283. b"passwordPASSWORDpassword",
  284. b"saltSALTsaltSALTsaltSALTsaltSALTsalt",
  285. 4096, 25,
  286. ),
  287. (
  288. hb("56fa6aa75548099dcc37d7f03425e0c3"),
  289. b"pass\00word", b"sa\00lt", 4096, 16,
  290. ),
  291. #
  292. # from example in http://grub.enbug.org/Authentication
  293. #
  294. (
  295. hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED"
  296. "97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC"
  297. "6C29E293F0A0"),
  298. b"hello",
  299. hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71"
  300. "784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073"
  301. "994D79080136"),
  302. 10000, 64, "sha512"
  303. ),
  304. #
  305. # test vectors from fastpbkdf2 <https://github.com/ctz/fastpbkdf2/blob/master/testdata.py>
  306. #
  307. (
  308. hb('55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc'
  309. '49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783'),
  310. b'passwd', b'salt', 1, 64, 'sha256',
  311. ),
  312. (
  313. hb('4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56'
  314. 'a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d'),
  315. b'Password', b'NaCl', 80000, 64, 'sha256',
  316. ),
  317. (
  318. hb('120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b'),
  319. b'password', b'salt', 1, 32, 'sha256',
  320. ),
  321. (
  322. hb('ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43'),
  323. b'password', b'salt', 2, 32, 'sha256',
  324. ),
  325. (
  326. hb('c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a'),
  327. b'password', b'salt', 4096, 32, 'sha256',
  328. ),
  329. (
  330. hb('348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c'
  331. '635518c7dac47e9'),
  332. b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt',
  333. 4096, 40, 'sha256',
  334. ),
  335. (
  336. hb('9e83f279c040f2a11aa4a02b24c418f2d3cb39560c9627fa4f47e3bcc2897c3d'),
  337. b'', b'salt', 1024, 32, 'sha256',
  338. ),
  339. (
  340. hb('ea5808411eb0c7e830deab55096cee582761e22a9bc034e3ece925225b07bf46'),
  341. b'password', b'', 1024, 32, 'sha256',
  342. ),
  343. (
  344. hb('89b69d0516f829893c696226650a8687'),
  345. b'pass\x00word', b'sa\x00lt', 4096, 16, 'sha256',
  346. ),
  347. (
  348. hb('867f70cf1ade02cff3752599a3a53dc4af34c7a669815ae5d513554e1c8cf252'),
  349. b'password', b'salt', 1, 32, 'sha512',
  350. ),
  351. (
  352. hb('e1d9c16aa681708a45f5c7c4e215ceb66e011a2e9f0040713f18aefdb866d53c'),
  353. b'password', b'salt', 2, 32, 'sha512',
  354. ),
  355. (
  356. hb('d197b1b33db0143e018b12f3d1d1479e6cdebdcc97c5c0f87f6902e072f457b5'),
  357. b'password', b'salt', 4096, 32, 'sha512',
  358. ),
  359. (
  360. hb('6e23f27638084b0f7ea1734e0d9841f55dd29ea60a834466f3396bac801fac1eeb'
  361. '63802f03a0b4acd7603e3699c8b74437be83ff01ad7f55dac1ef60f4d56480c35e'
  362. 'e68fd52c6936'),
  363. b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt',
  364. 1, 72, 'sha512',
  365. ),
  366. (
  367. hb('0c60c80f961f0e71f3a9b524af6012062fe037a6'),
  368. b'password', b'salt', 1, 20, 'sha1',
  369. ),
  370. #
  371. # custom tests
  372. #
  373. (
  374. hb('e248fb6b13365146f8ac6307cc222812'),
  375. b"secret", b"salt", 10, 16, "sha1",
  376. ),
  377. (
  378. hb('e248fb6b13365146f8ac6307cc2228127872da6d'),
  379. b"secret", b"salt", 10, None, "sha1",
  380. ),
  381. (
  382. hb('b1d5485772e6f76d5ebdc11b38d3eff0a5b2bd50dc11f937e86ecacd0cd40d1b'
  383. '9113e0734e3b76a3'),
  384. b"secret", b"salt", 62, 40, "md5",
  385. ),
  386. (
  387. hb('ea014cc01f78d3883cac364bb5d054e2be238fb0b6081795a9d84512126e3129'
  388. '062104d2183464c4'),
  389. b"secret", b"salt", 62, 40, "md4",
  390. ),
  391. ]
  392. def test_known(self):
  393. """test reference vectors"""
  394. for row in self.pbkdf2_test_vectors:
  395. correct, secret, salt, rounds, keylen = row[:5]
  396. digest = row[5] if len(row) == 6 else "sha1"
  397. result = pbkdf2_hmac(digest, secret, salt, rounds, keylen)
  398. self.assertEqual(result, correct)
  399. def test_backends(self):
  400. """verify expected backends are present"""
  401. from passlib.crypto.digest import PBKDF2_BACKENDS
  402. # check for fastpbkdf2
  403. try:
  404. import fastpbkdf2
  405. has_fastpbkdf2 = True
  406. except ImportError:
  407. has_fastpbkdf2 = False
  408. self.assertEqual("fastpbkdf2" in PBKDF2_BACKENDS, has_fastpbkdf2)
  409. # check for hashlib
  410. try:
  411. from hashlib import pbkdf2_hmac
  412. has_hashlib_ssl = pbkdf2_hmac.__module__ != "hashlib"
  413. except ImportError:
  414. has_hashlib_ssl = False
  415. self.assertEqual("hashlib-ssl" in PBKDF2_BACKENDS, has_hashlib_ssl)
  416. # check for appropriate builtin
  417. from passlib.utils.compat import PY3
  418. if PY3:
  419. self.assertIn("builtin-from-bytes", PBKDF2_BACKENDS)
  420. else:
  421. # XXX: only true as long as this is preferred over hexlify
  422. self.assertIn("builtin-unpack", PBKDF2_BACKENDS)
  423. def test_border(self):
  424. """test border cases"""
  425. def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, digest="sha1"):
  426. return pbkdf2_hmac(digest, secret, salt, rounds, keylen)
  427. helper()
  428. # invalid rounds
  429. self.assertRaises(ValueError, helper, rounds=-1)
  430. self.assertRaises(ValueError, helper, rounds=0)
  431. self.assertRaises(TypeError, helper, rounds='x')
  432. # invalid keylen
  433. helper(keylen=1)
  434. self.assertRaises(ValueError, helper, keylen=-1)
  435. self.assertRaises(ValueError, helper, keylen=0)
  436. # NOTE: hashlib actually throws error for keylen>=MAX_SINT32,
  437. # but pbkdf2 forbids anything > MAX_UINT32 * digest_size
  438. self.assertRaises(OverflowError, helper, keylen=20*(2**32-1)+1)
  439. self.assertRaises(TypeError, helper, keylen='x')
  440. # invalid secret/salt type
  441. self.assertRaises(TypeError, helper, salt=5)
  442. self.assertRaises(TypeError, helper, secret=5)
  443. # invalid hash
  444. self.assertRaises(ValueError, helper, digest='foo')
  445. self.assertRaises(TypeError, helper, digest=5)
  446. def test_default_keylen(self):
  447. """test keylen==None"""
  448. def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, digest="sha1"):
  449. return pbkdf2_hmac(digest, secret, salt, rounds, keylen)
  450. self.assertEqual(len(helper(digest='sha1')), 20)
  451. self.assertEqual(len(helper(digest='sha256')), 32)
  452. #=============================================================================
  453. # eof
  454. #=============================================================================