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

1024 lines
39 KiB

  1. """tests for passlib.util"""
  2. #=============================================================================
  3. # imports
  4. #=============================================================================
  5. from __future__ import with_statement
  6. # core
  7. from functools import partial
  8. import warnings
  9. # site
  10. # pkg
  11. # module
  12. from passlib.utils import is_ascii_safe
  13. from passlib.utils.compat import irange, PY2, PY3, u, unicode, join_bytes, PYPY
  14. from passlib.tests.utils import TestCase, hb, run_with_fixed_seeds
  15. #=============================================================================
  16. # byte funcs
  17. #=============================================================================
  18. class MiscTest(TestCase):
  19. """tests various parts of utils module"""
  20. # NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test
  21. def test_compat(self):
  22. """test compat's lazymodule"""
  23. from passlib.utils import compat
  24. # "<module 'passlib.utils.compat' from 'passlib/utils/compat.pyc'>"
  25. self.assertRegex(repr(compat),
  26. r"^<module 'passlib.utils.compat' from '.*?'>$")
  27. # test synthentic dir()
  28. dir(compat)
  29. self.assertTrue('UnicodeIO' in dir(compat))
  30. self.assertTrue('irange' in dir(compat))
  31. def test_classproperty(self):
  32. from passlib.utils.decor import classproperty
  33. class test(object):
  34. xvar = 1
  35. @classproperty
  36. def xprop(cls):
  37. return cls.xvar
  38. self.assertEqual(test.xprop, 1)
  39. prop = test.__dict__['xprop']
  40. self.assertIs(prop.im_func, prop.__func__)
  41. def test_deprecated_function(self):
  42. from passlib.utils.decor import deprecated_function
  43. # NOTE: not comprehensive, just tests the basic behavior
  44. @deprecated_function(deprecated="1.6", removed="1.8")
  45. def test_func(*args):
  46. """test docstring"""
  47. return args
  48. self.assertTrue(".. deprecated::" in test_func.__doc__)
  49. with self.assertWarningList(dict(category=DeprecationWarning,
  50. message="the function passlib.tests.test_utils.test_func() "
  51. "is deprecated as of Passlib 1.6, and will be "
  52. "removed in Passlib 1.8."
  53. )):
  54. self.assertEqual(test_func(1,2), (1,2))
  55. def test_memoized_property(self):
  56. from passlib.utils.decor import memoized_property
  57. class dummy(object):
  58. counter = 0
  59. @memoized_property
  60. def value(self):
  61. value = self.counter
  62. self.counter = value+1
  63. return value
  64. d = dummy()
  65. self.assertEqual(d.value, 0)
  66. self.assertEqual(d.value, 0)
  67. self.assertEqual(d.counter, 1)
  68. prop = dummy.value
  69. if not PY3:
  70. self.assertIs(prop.im_func, prop.__func__)
  71. def test_getrandbytes(self):
  72. """getrandbytes()"""
  73. from passlib.utils import getrandbytes
  74. wrapper = partial(getrandbytes, self.getRandom())
  75. self.assertEqual(len(wrapper(0)), 0)
  76. a = wrapper(10)
  77. b = wrapper(10)
  78. self.assertIsInstance(a, bytes)
  79. self.assertEqual(len(a), 10)
  80. self.assertEqual(len(b), 10)
  81. self.assertNotEqual(a, b)
  82. @run_with_fixed_seeds(count=1024)
  83. def test_getrandstr(self, seed):
  84. """getrandstr()"""
  85. from passlib.utils import getrandstr
  86. wrapper = partial(getrandstr, self.getRandom(seed=seed))
  87. # count 0
  88. self.assertEqual(wrapper('abc',0), '')
  89. # count <0
  90. self.assertRaises(ValueError, wrapper, 'abc', -1)
  91. # letters 0
  92. self.assertRaises(ValueError, wrapper, '', 0)
  93. # letters 1
  94. self.assertEqual(wrapper('a', 5), 'aaaaa')
  95. # NOTE: the following parts are non-deterministic,
  96. # with a small chance of failure (outside chance it may pick
  97. # a string w/o one char, even more remote chance of picking
  98. # same string). to combat this, we run it against multiple
  99. # fixed seeds (using run_with_fixed_seeds decorator),
  100. # and hope that they're sufficient to test the range of behavior.
  101. # letters
  102. x = wrapper(u('abc'), 32)
  103. y = wrapper(u('abc'), 32)
  104. self.assertIsInstance(x, unicode)
  105. self.assertNotEqual(x,y)
  106. self.assertEqual(sorted(set(x)), [u('a'),u('b'),u('c')])
  107. # bytes
  108. x = wrapper(b'abc', 32)
  109. y = wrapper(b'abc', 32)
  110. self.assertIsInstance(x, bytes)
  111. self.assertNotEqual(x,y)
  112. # NOTE: decoding this due to py3 bytes
  113. self.assertEqual(sorted(set(x.decode("ascii"))), [u('a'),u('b'),u('c')])
  114. def test_generate_password(self):
  115. """generate_password()"""
  116. from passlib.utils import generate_password
  117. warnings.filterwarnings("ignore", "The function.*generate_password\(\) is deprecated")
  118. self.assertEqual(len(generate_password(15)), 15)
  119. def test_is_crypt_context(self):
  120. """test is_crypt_context()"""
  121. from passlib.utils import is_crypt_context
  122. from passlib.context import CryptContext
  123. cc = CryptContext(["des_crypt"])
  124. self.assertTrue(is_crypt_context(cc))
  125. self.assertFalse(not is_crypt_context(cc))
  126. def test_genseed(self):
  127. """test genseed()"""
  128. import random
  129. from passlib.utils import genseed
  130. rng = random.Random(genseed())
  131. a = rng.randint(0, 10**10)
  132. rng = random.Random(genseed())
  133. b = rng.randint(0, 10**10)
  134. self.assertNotEqual(a,b)
  135. rng.seed(genseed(rng))
  136. def test_crypt(self):
  137. """test crypt.crypt() wrappers"""
  138. from passlib.utils import has_crypt, safe_crypt, test_crypt
  139. # test everything is disabled
  140. if not has_crypt:
  141. self.assertEqual(safe_crypt("test", "aa"), None)
  142. self.assertFalse(test_crypt("test", "aaqPiZY5xR5l."))
  143. raise self.skipTest("crypt.crypt() not available")
  144. # XXX: this assumes *every* crypt() implementation supports des_crypt.
  145. # if this fails for some platform, this test will need modifying.
  146. # test return type
  147. self.assertIsInstance(safe_crypt(u("test"), u("aa")), unicode)
  148. # test ascii password
  149. h1 = u('aaqPiZY5xR5l.')
  150. self.assertEqual(safe_crypt(u('test'), u('aa')), h1)
  151. self.assertEqual(safe_crypt(b'test', b'aa'), h1)
  152. # test utf-8 / unicode password
  153. h2 = u('aahWwbrUsKZk.')
  154. self.assertEqual(safe_crypt(u('test\u1234'), 'aa'), h2)
  155. self.assertEqual(safe_crypt(b'test\xe1\x88\xb4', 'aa'), h2)
  156. # test latin-1 password
  157. hash = safe_crypt(b'test\xff', 'aa')
  158. if PY3: # py3 supports utf-8 bytes only.
  159. self.assertEqual(hash, None)
  160. else: # but py2 is fine.
  161. self.assertEqual(hash, u('aaOx.5nbTU/.M'))
  162. # test rejects null chars in password
  163. self.assertRaises(ValueError, safe_crypt, '\x00', 'aa')
  164. # check test_crypt()
  165. h1x = h1[:-1] + 'x'
  166. self.assertTrue(test_crypt("test", h1))
  167. self.assertFalse(test_crypt("test", h1x))
  168. # check crypt returning variant error indicators
  169. # some platforms return None on errors, others empty string,
  170. # The BSDs in some cases return ":"
  171. import passlib.utils as mod
  172. orig = mod._crypt
  173. try:
  174. fake = None
  175. mod._crypt = lambda secret, hash: fake
  176. for fake in [None, "", ":", ":0", "*0"]:
  177. self.assertEqual(safe_crypt("test", "aa"), None)
  178. self.assertFalse(test_crypt("test", h1))
  179. fake = 'xxx'
  180. self.assertEqual(safe_crypt("test", "aa"), "xxx")
  181. finally:
  182. mod._crypt = orig
  183. def test_consteq(self):
  184. """test consteq()"""
  185. # NOTE: this test is kind of over the top, but that's only because
  186. # this is used for the critical task of comparing hashes for equality.
  187. from passlib.utils import consteq, str_consteq
  188. # ensure error raises for wrong types
  189. self.assertRaises(TypeError, consteq, u(''), b'')
  190. self.assertRaises(TypeError, consteq, u(''), 1)
  191. self.assertRaises(TypeError, consteq, u(''), None)
  192. self.assertRaises(TypeError, consteq, b'', u(''))
  193. self.assertRaises(TypeError, consteq, b'', 1)
  194. self.assertRaises(TypeError, consteq, b'', None)
  195. self.assertRaises(TypeError, consteq, None, u(''))
  196. self.assertRaises(TypeError, consteq, None, b'')
  197. self.assertRaises(TypeError, consteq, 1, u(''))
  198. self.assertRaises(TypeError, consteq, 1, b'')
  199. def consteq_supports_string(value):
  200. # under PY2, it supports all unicode strings (when present at all),
  201. # under PY3, compare_digest() only supports ascii unicode strings.
  202. # confirmed for: cpython 2.7.9, cpython 3.4, pypy, pypy3, pyston
  203. return (consteq is str_consteq or PY2 or is_ascii_safe(value))
  204. # check equal inputs compare correctly
  205. for value in [
  206. u("a"),
  207. u("abc"),
  208. u("\xff\xa2\x12\x00")*10,
  209. ]:
  210. if consteq_supports_string(value):
  211. self.assertTrue(consteq(value, value), "value %r:" % (value,))
  212. else:
  213. self.assertRaises(TypeError, consteq, value, value)
  214. self.assertTrue(str_consteq(value, value), "value %r:" % (value,))
  215. value = value.encode("latin-1")
  216. self.assertTrue(consteq(value, value), "value %r:" % (value,))
  217. # check non-equal inputs compare correctly
  218. for l,r in [
  219. # check same-size comparisons with differing contents fail.
  220. (u("a"), u("c")),
  221. (u("abcabc"), u("zbaabc")),
  222. (u("abcabc"), u("abzabc")),
  223. (u("abcabc"), u("abcabz")),
  224. ((u("\xff\xa2\x12\x00")*10)[:-1] + u("\x01"),
  225. u("\xff\xa2\x12\x00")*10),
  226. # check different-size comparisons fail.
  227. (u(""), u("a")),
  228. (u("abc"), u("abcdef")),
  229. (u("abc"), u("defabc")),
  230. (u("qwertyuiopasdfghjklzxcvbnm"), u("abc")),
  231. ]:
  232. if consteq_supports_string(l) and consteq_supports_string(r):
  233. self.assertFalse(consteq(l, r), "values %r %r:" % (l,r))
  234. self.assertFalse(consteq(r, l), "values %r %r:" % (r,l))
  235. else:
  236. self.assertRaises(TypeError, consteq, l, r)
  237. self.assertRaises(TypeError, consteq, r, l)
  238. self.assertFalse(str_consteq(l, r), "values %r %r:" % (l,r))
  239. self.assertFalse(str_consteq(r, l), "values %r %r:" % (r,l))
  240. l = l.encode("latin-1")
  241. r = r.encode("latin-1")
  242. self.assertFalse(consteq(l, r), "values %r %r:" % (l,r))
  243. self.assertFalse(consteq(r, l), "values %r %r:" % (r,l))
  244. # TODO: add some tests to ensure we take THETA(strlen) time.
  245. # this might be hard to do reproducably.
  246. # NOTE: below code was used to generate stats for analysis
  247. ##from math import log as logb
  248. ##import timeit
  249. ##multipliers = [ 1<<s for s in irange(9)]
  250. ##correct = u"abcdefgh"*(1<<4)
  251. ##incorrect = u"abcdxfgh"
  252. ##print
  253. ##first = True
  254. ##for run in irange(1):
  255. ## times = []
  256. ## chars = []
  257. ## for m in multipliers:
  258. ## supplied = incorrect * m
  259. ## def test():
  260. ## self.assertFalse(consteq(supplied,correct))
  261. ## ##self.assertFalse(supplied == correct)
  262. ## times.append(timeit.timeit(test, number=100000))
  263. ## chars.append(len(supplied))
  264. ## # output for wolfram alpha
  265. ## print ", ".join("{%r, %r}" % (c,round(t,4)) for c,t in zip(chars,times))
  266. ## def scale(c):
  267. ## return logb(c,2)
  268. ## print ", ".join("{%r, %r}" % (scale(c),round(t,4)) for c,t in zip(chars,times))
  269. ## # output for spreadsheet
  270. ## ##if first:
  271. ## ## print "na, " + ", ".join(str(c) for c in chars)
  272. ## ## first = False
  273. ## ##print ", ".join(str(c) for c in [run] + times)
  274. def test_saslprep(self):
  275. """test saslprep() unicode normalizer"""
  276. self.require_stringprep()
  277. from passlib.utils import saslprep as sp
  278. # invalid types
  279. self.assertRaises(TypeError, sp, None)
  280. self.assertRaises(TypeError, sp, 1)
  281. self.assertRaises(TypeError, sp, b'')
  282. # empty strings
  283. self.assertEqual(sp(u('')), u(''))
  284. self.assertEqual(sp(u('\u00AD')), u(''))
  285. # verify B.1 chars are stripped,
  286. self.assertEqual(sp(u("$\u00AD$\u200D$")), u("$$$"))
  287. # verify C.1.2 chars are replaced with space
  288. self.assertEqual(sp(u("$ $\u00A0$\u3000$")), u("$ $ $ $"))
  289. # verify normalization to KC
  290. self.assertEqual(sp(u("a\u0300")), u("\u00E0"))
  291. self.assertEqual(sp(u("\u00E0")), u("\u00E0"))
  292. # verify various forbidden characters
  293. # control chars
  294. self.assertRaises(ValueError, sp, u("\u0000"))
  295. self.assertRaises(ValueError, sp, u("\u007F"))
  296. self.assertRaises(ValueError, sp, u("\u180E"))
  297. self.assertRaises(ValueError, sp, u("\uFFF9"))
  298. # private use
  299. self.assertRaises(ValueError, sp, u("\uE000"))
  300. # non-characters
  301. self.assertRaises(ValueError, sp, u("\uFDD0"))
  302. # surrogates
  303. self.assertRaises(ValueError, sp, u("\uD800"))
  304. # non-plaintext chars
  305. self.assertRaises(ValueError, sp, u("\uFFFD"))
  306. # non-canon
  307. self.assertRaises(ValueError, sp, u("\u2FF0"))
  308. # change display properties
  309. self.assertRaises(ValueError, sp, u("\u200E"))
  310. self.assertRaises(ValueError, sp, u("\u206F"))
  311. # unassigned code points (as of unicode 3.2)
  312. self.assertRaises(ValueError, sp, u("\u0900"))
  313. self.assertRaises(ValueError, sp, u("\uFFF8"))
  314. # tagging characters
  315. self.assertRaises(ValueError, sp, u("\U000e0001"))
  316. # verify bidi behavior
  317. # if starts with R/AL -- must end with R/AL
  318. self.assertRaises(ValueError, sp, u("\u0627\u0031"))
  319. self.assertEqual(sp(u("\u0627")), u("\u0627"))
  320. self.assertEqual(sp(u("\u0627\u0628")), u("\u0627\u0628"))
  321. self.assertEqual(sp(u("\u0627\u0031\u0628")), u("\u0627\u0031\u0628"))
  322. # if starts with R/AL -- cannot contain L
  323. self.assertRaises(ValueError, sp, u("\u0627\u0041\u0628"))
  324. # if doesn't start with R/AL -- can contain R/AL, but L & EN allowed
  325. self.assertRaises(ValueError, sp, u("x\u0627z"))
  326. self.assertEqual(sp(u("x\u0041z")), u("x\u0041z"))
  327. #------------------------------------------------------
  328. # examples pulled from external sources, to be thorough
  329. #------------------------------------------------------
  330. # rfc 4031 section 3 examples
  331. self.assertEqual(sp(u("I\u00ADX")), u("IX")) # strip SHY
  332. self.assertEqual(sp(u("user")), u("user")) # unchanged
  333. self.assertEqual(sp(u("USER")), u("USER")) # case preserved
  334. self.assertEqual(sp(u("\u00AA")), u("a")) # normalize to KC form
  335. self.assertEqual(sp(u("\u2168")), u("IX")) # normalize to KC form
  336. self.assertRaises(ValueError, sp, u("\u0007")) # forbid control chars
  337. self.assertRaises(ValueError, sp, u("\u0627\u0031")) # invalid bidi
  338. # rfc 3454 section 6 examples
  339. # starts with RAL char, must end with RAL char
  340. self.assertRaises(ValueError, sp, u("\u0627\u0031"))
  341. self.assertEqual(sp(u("\u0627\u0031\u0628")), u("\u0627\u0031\u0628"))
  342. def test_splitcomma(self):
  343. from passlib.utils import splitcomma
  344. self.assertEqual(splitcomma(""), [])
  345. self.assertEqual(splitcomma(","), [])
  346. self.assertEqual(splitcomma("a"), ['a'])
  347. self.assertEqual(splitcomma(" a , "), ['a'])
  348. self.assertEqual(splitcomma(" a , b"), ['a', 'b'])
  349. self.assertEqual(splitcomma(" a, b, "), ['a', 'b'])
  350. #=============================================================================
  351. # byte/unicode helpers
  352. #=============================================================================
  353. class CodecTest(TestCase):
  354. """tests bytes/unicode helpers in passlib.utils"""
  355. def test_bytes(self):
  356. """test b() helper, bytes and native str type"""
  357. if PY3:
  358. import builtins
  359. self.assertIs(bytes, builtins.bytes)
  360. else:
  361. import __builtin__ as builtins
  362. self.assertIs(bytes, builtins.str)
  363. self.assertIsInstance(b'', bytes)
  364. self.assertIsInstance(b'\x00\xff', bytes)
  365. if PY3:
  366. self.assertEqual(b'\x00\xff'.decode("latin-1"), "\x00\xff")
  367. else:
  368. self.assertEqual(b'\x00\xff', "\x00\xff")
  369. def test_to_bytes(self):
  370. """test to_bytes()"""
  371. from passlib.utils import to_bytes
  372. # check unicode inputs
  373. self.assertEqual(to_bytes(u('abc')), b'abc')
  374. self.assertEqual(to_bytes(u('\x00\xff')), b'\x00\xc3\xbf')
  375. # check unicode w/ encodings
  376. self.assertEqual(to_bytes(u('\x00\xff'), 'latin-1'), b'\x00\xff')
  377. self.assertRaises(ValueError, to_bytes, u('\x00\xff'), 'ascii')
  378. # check bytes inputs
  379. self.assertEqual(to_bytes(b'abc'), b'abc')
  380. self.assertEqual(to_bytes(b'\x00\xff'), b'\x00\xff')
  381. self.assertEqual(to_bytes(b'\x00\xc3\xbf'), b'\x00\xc3\xbf')
  382. # check byte inputs ignores enocding
  383. self.assertEqual(to_bytes(b'\x00\xc3\xbf', "latin-1"),
  384. b'\x00\xc3\xbf')
  385. # check bytes transcoding
  386. self.assertEqual(to_bytes(b'\x00\xc3\xbf', "latin-1", "", "utf-8"),
  387. b'\x00\xff')
  388. # check other
  389. self.assertRaises(AssertionError, to_bytes, 'abc', None)
  390. self.assertRaises(TypeError, to_bytes, None)
  391. def test_to_unicode(self):
  392. """test to_unicode()"""
  393. from passlib.utils import to_unicode
  394. # check unicode inputs
  395. self.assertEqual(to_unicode(u('abc')), u('abc'))
  396. self.assertEqual(to_unicode(u('\x00\xff')), u('\x00\xff'))
  397. # check unicode input ignores encoding
  398. self.assertEqual(to_unicode(u('\x00\xff'), "ascii"), u('\x00\xff'))
  399. # check bytes input
  400. self.assertEqual(to_unicode(b'abc'), u('abc'))
  401. self.assertEqual(to_unicode(b'\x00\xc3\xbf'), u('\x00\xff'))
  402. self.assertEqual(to_unicode(b'\x00\xff', 'latin-1'),
  403. u('\x00\xff'))
  404. self.assertRaises(ValueError, to_unicode, b'\x00\xff')
  405. # check other
  406. self.assertRaises(AssertionError, to_unicode, 'abc', None)
  407. self.assertRaises(TypeError, to_unicode, None)
  408. def test_to_native_str(self):
  409. """test to_native_str()"""
  410. from passlib.utils import to_native_str
  411. # test plain ascii
  412. self.assertEqual(to_native_str(u('abc'), 'ascii'), 'abc')
  413. self.assertEqual(to_native_str(b'abc', 'ascii'), 'abc')
  414. # test invalid ascii
  415. if PY3:
  416. self.assertEqual(to_native_str(u('\xE0'), 'ascii'), '\xE0')
  417. self.assertRaises(UnicodeDecodeError, to_native_str, b'\xC3\xA0',
  418. 'ascii')
  419. else:
  420. self.assertRaises(UnicodeEncodeError, to_native_str, u('\xE0'),
  421. 'ascii')
  422. self.assertEqual(to_native_str(b'\xC3\xA0', 'ascii'), '\xC3\xA0')
  423. # test latin-1
  424. self.assertEqual(to_native_str(u('\xE0'), 'latin-1'), '\xE0')
  425. self.assertEqual(to_native_str(b'\xE0', 'latin-1'), '\xE0')
  426. # test utf-8
  427. self.assertEqual(to_native_str(u('\xE0'), 'utf-8'),
  428. '\xE0' if PY3 else '\xC3\xA0')
  429. self.assertEqual(to_native_str(b'\xC3\xA0', 'utf-8'),
  430. '\xE0' if PY3 else '\xC3\xA0')
  431. # other types rejected
  432. self.assertRaises(TypeError, to_native_str, None, 'ascii')
  433. def test_is_ascii_safe(self):
  434. """test is_ascii_safe()"""
  435. from passlib.utils import is_ascii_safe
  436. self.assertTrue(is_ascii_safe(b"\x00abc\x7f"))
  437. self.assertTrue(is_ascii_safe(u("\x00abc\x7f")))
  438. self.assertFalse(is_ascii_safe(b"\x00abc\x80"))
  439. self.assertFalse(is_ascii_safe(u("\x00abc\x80")))
  440. def test_is_same_codec(self):
  441. """test is_same_codec()"""
  442. from passlib.utils import is_same_codec
  443. self.assertTrue(is_same_codec(None, None))
  444. self.assertFalse(is_same_codec(None, 'ascii'))
  445. self.assertTrue(is_same_codec("ascii", "ascii"))
  446. self.assertTrue(is_same_codec("ascii", "ASCII"))
  447. self.assertTrue(is_same_codec("utf-8", "utf-8"))
  448. self.assertTrue(is_same_codec("utf-8", "utf8"))
  449. self.assertTrue(is_same_codec("utf-8", "UTF_8"))
  450. self.assertFalse(is_same_codec("ascii", "utf-8"))
  451. #=============================================================================
  452. # base64engine
  453. #=============================================================================
  454. class Base64EngineTest(TestCase):
  455. """test standalone parts of Base64Engine"""
  456. # NOTE: most Base64Engine testing done via _Base64Test subclasses below.
  457. def test_constructor(self):
  458. from passlib.utils.binary import Base64Engine, AB64_CHARS
  459. # bad charmap type
  460. self.assertRaises(TypeError, Base64Engine, 1)
  461. # bad charmap size
  462. self.assertRaises(ValueError, Base64Engine, AB64_CHARS[:-1])
  463. # dup charmap letter
  464. self.assertRaises(ValueError, Base64Engine, AB64_CHARS[:-1] + "A")
  465. def test_ab64_decode(self):
  466. """ab64_decode()"""
  467. from passlib.utils.binary import ab64_decode
  468. # accept bytes or unicode
  469. self.assertEqual(ab64_decode(b"abc"), hb("69b7"))
  470. self.assertEqual(ab64_decode(u("abc")), hb("69b7"))
  471. # reject non-ascii unicode
  472. self.assertRaises(ValueError, ab64_decode, u("ab\xff"))
  473. # underlying a2b_ascii treats non-base64 chars as "Incorrect padding"
  474. self.assertRaises(TypeError, ab64_decode, b"ab\xff")
  475. self.assertRaises(TypeError, ab64_decode, b"ab!")
  476. self.assertRaises(TypeError, ab64_decode, u("ab!"))
  477. # insert correct padding, handle dirty padding bits
  478. self.assertEqual(ab64_decode(b"abcd"), hb("69b71d")) # 0 mod 4
  479. self.assertRaises(ValueError, ab64_decode, b"abcde") # 1 mod 4
  480. self.assertEqual(ab64_decode(b"abcdef"), hb("69b71d79")) # 2 mod 4, dirty padding bits
  481. self.assertEqual(ab64_decode(b"abcdeQ"), hb("69b71d79")) # 2 mod 4, clean padding bits
  482. self.assertEqual(ab64_decode(b"abcdefg"), hb("69b71d79f8")) # 3 mod 4, clean padding bits
  483. # support "./" or "+/" altchars
  484. # (lets us transition to "+/" representation, merge w/ b64s_decode)
  485. self.assertEqual(ab64_decode(b"ab+/"), hb("69bfbf"))
  486. self.assertEqual(ab64_decode(b"ab./"), hb("69bfbf"))
  487. def test_ab64_encode(self):
  488. """ab64_encode()"""
  489. from passlib.utils.binary import ab64_encode
  490. # accept bytes
  491. self.assertEqual(ab64_encode(hb("69b7")), b"abc")
  492. # reject unicode
  493. self.assertRaises(TypeError if PY3 else UnicodeEncodeError,
  494. ab64_encode, hb("69b7").decode("latin-1"))
  495. # insert correct padding before decoding
  496. self.assertEqual(ab64_encode(hb("69b71d")), b"abcd") # 0 mod 4
  497. self.assertEqual(ab64_encode(hb("69b71d79")), b"abcdeQ") # 2 mod 4
  498. self.assertEqual(ab64_encode(hb("69b71d79f8")), b"abcdefg") # 3 mod 4
  499. # output "./" altchars
  500. self.assertEqual(ab64_encode(hb("69bfbf")), b"ab./")
  501. def test_b64s_decode(self):
  502. """b64s_decode()"""
  503. from passlib.utils.binary import b64s_decode
  504. # accept bytes or unicode
  505. self.assertEqual(b64s_decode(b"abc"), hb("69b7"))
  506. self.assertEqual(b64s_decode(u("abc")), hb("69b7"))
  507. # reject non-ascii unicode
  508. self.assertRaises(ValueError, b64s_decode, u("ab\xff"))
  509. # underlying a2b_ascii treats non-base64 chars as "Incorrect padding"
  510. self.assertRaises(TypeError, b64s_decode, b"ab\xff")
  511. self.assertRaises(TypeError, b64s_decode, b"ab!")
  512. self.assertRaises(TypeError, b64s_decode, u("ab!"))
  513. # insert correct padding, handle dirty padding bits
  514. self.assertEqual(b64s_decode(b"abcd"), hb("69b71d")) # 0 mod 4
  515. self.assertRaises(ValueError, b64s_decode, b"abcde") # 1 mod 4
  516. self.assertEqual(b64s_decode(b"abcdef"), hb("69b71d79")) # 2 mod 4, dirty padding bits
  517. self.assertEqual(b64s_decode(b"abcdeQ"), hb("69b71d79")) # 2 mod 4, clean padding bits
  518. self.assertEqual(b64s_decode(b"abcdefg"), hb("69b71d79f8")) # 3 mod 4, clean padding bits
  519. def test_b64s_encode(self):
  520. """b64s_encode()"""
  521. from passlib.utils.binary import b64s_encode
  522. # accept bytes
  523. self.assertEqual(b64s_encode(hb("69b7")), b"abc")
  524. # reject unicode
  525. self.assertRaises(TypeError if PY3 else UnicodeEncodeError,
  526. b64s_encode, hb("69b7").decode("latin-1"))
  527. # insert correct padding before decoding
  528. self.assertEqual(b64s_encode(hb("69b71d")), b"abcd") # 0 mod 4
  529. self.assertEqual(b64s_encode(hb("69b71d79")), b"abcdeQ") # 2 mod 4
  530. self.assertEqual(b64s_encode(hb("69b71d79f8")), b"abcdefg") # 3 mod 4
  531. # output "+/" altchars
  532. self.assertEqual(b64s_encode(hb("69bfbf")), b"ab+/")
  533. class _Base64Test(TestCase):
  534. """common tests for all Base64Engine instances"""
  535. #===================================================================
  536. # class attrs
  537. #===================================================================
  538. # Base64Engine instance to test
  539. engine = None
  540. # pairs of (raw, encoded) bytes to test - should encode/decode correctly
  541. encoded_data = None
  542. # tuples of (encoded, value, bits) for known integer encodings
  543. encoded_ints = None
  544. # invalid encoded byte
  545. bad_byte = b"?"
  546. # helper to generate bytemap-specific strings
  547. def m(self, *offsets):
  548. """generate byte string from offsets"""
  549. return join_bytes(self.engine.bytemap[o:o+1] for o in offsets)
  550. #===================================================================
  551. # test encode_bytes
  552. #===================================================================
  553. def test_encode_bytes(self):
  554. """test encode_bytes() against reference inputs"""
  555. engine = self.engine
  556. encode = engine.encode_bytes
  557. for raw, encoded in self.encoded_data:
  558. result = encode(raw)
  559. self.assertEqual(result, encoded, "encode %r:" % (raw,))
  560. def test_encode_bytes_bad(self):
  561. """test encode_bytes() with bad input"""
  562. engine = self.engine
  563. encode = engine.encode_bytes
  564. self.assertRaises(TypeError, encode, u('\x00'))
  565. self.assertRaises(TypeError, encode, None)
  566. #===================================================================
  567. # test decode_bytes
  568. #===================================================================
  569. def test_decode_bytes(self):
  570. """test decode_bytes() against reference inputs"""
  571. engine = self.engine
  572. decode = engine.decode_bytes
  573. for raw, encoded in self.encoded_data:
  574. result = decode(encoded)
  575. self.assertEqual(result, raw, "decode %r:" % (encoded,))
  576. def test_decode_bytes_padding(self):
  577. """test decode_bytes() ignores padding bits"""
  578. bchr = (lambda v: bytes([v])) if PY3 else chr
  579. engine = self.engine
  580. m = self.m
  581. decode = engine.decode_bytes
  582. BNULL = b"\x00"
  583. # length == 2 mod 4: 4 bits of padding
  584. self.assertEqual(decode(m(0,0)), BNULL)
  585. for i in range(0,6):
  586. if engine.big: # 4 lsb padding
  587. correct = BNULL if i < 4 else bchr(1<<(i-4))
  588. else: # 4 msb padding
  589. correct = bchr(1<<(i+6)) if i < 2 else BNULL
  590. self.assertEqual(decode(m(0,1<<i)), correct, "%d/4 bits:" % i)
  591. # length == 3 mod 4: 2 bits of padding
  592. self.assertEqual(decode(m(0,0,0)), BNULL*2)
  593. for i in range(0,6):
  594. if engine.big: # 2 lsb are padding
  595. correct = BNULL if i < 2 else bchr(1<<(i-2))
  596. else: # 2 msg are padding
  597. correct = bchr(1<<(i+4)) if i < 4 else BNULL
  598. self.assertEqual(decode(m(0,0,1<<i)), BNULL + correct,
  599. "%d/2 bits:" % i)
  600. def test_decode_bytes_bad(self):
  601. """test decode_bytes() with bad input"""
  602. engine = self.engine
  603. decode = engine.decode_bytes
  604. # wrong size (1 % 4)
  605. self.assertRaises(ValueError, decode, engine.bytemap[:5])
  606. # wrong char
  607. self.assertTrue(self.bad_byte not in engine.bytemap)
  608. self.assertRaises(ValueError, decode, self.bad_byte*4)
  609. # wrong type
  610. self.assertRaises(TypeError, decode, engine.charmap[:4])
  611. self.assertRaises(TypeError, decode, None)
  612. #===================================================================
  613. # encode_bytes+decode_bytes
  614. #===================================================================
  615. def test_codec(self):
  616. """test encode_bytes/decode_bytes against random data"""
  617. engine = self.engine
  618. from passlib.utils import getrandbytes, getrandstr
  619. rng = self.getRandom()
  620. saw_zero = False
  621. for i in irange(500):
  622. #
  623. # test raw -> encode() -> decode() -> raw
  624. #
  625. # generate some random bytes
  626. size = rng.randint(1 if saw_zero else 0, 12)
  627. if not size:
  628. saw_zero = True
  629. enc_size = (4*size+2)//3
  630. raw = getrandbytes(rng, size)
  631. # encode them, check invariants
  632. encoded = engine.encode_bytes(raw)
  633. self.assertEqual(len(encoded), enc_size)
  634. # make sure decode returns original
  635. result = engine.decode_bytes(encoded)
  636. self.assertEqual(result, raw)
  637. #
  638. # test encoded -> decode() -> encode() -> encoded
  639. #
  640. # generate some random encoded data
  641. if size % 4 == 1:
  642. size += rng.choice([-1,1,2])
  643. raw_size = 3*size//4
  644. encoded = getrandstr(rng, engine.bytemap, size)
  645. # decode them, check invariants
  646. raw = engine.decode_bytes(encoded)
  647. self.assertEqual(len(raw), raw_size, "encoded %d:" % size)
  648. # make sure encode returns original (barring padding bits)
  649. result = engine.encode_bytes(raw)
  650. if size % 4:
  651. self.assertEqual(result[:-1], encoded[:-1])
  652. else:
  653. self.assertEqual(result, encoded)
  654. def test_repair_unused(self):
  655. """test repair_unused()"""
  656. # NOTE: this test relies on encode_bytes() always returning clear
  657. # padding bits - which should be ensured by test vectors.
  658. from passlib.utils import getrandstr
  659. rng = self.getRandom()
  660. engine = self.engine
  661. check_repair_unused = self.engine.check_repair_unused
  662. i = 0
  663. while i < 300:
  664. size = rng.randint(0,23)
  665. cdata = getrandstr(rng, engine.charmap, size).encode("ascii")
  666. if size & 3 == 1:
  667. # should throw error
  668. self.assertRaises(ValueError, check_repair_unused, cdata)
  669. continue
  670. rdata = engine.encode_bytes(engine.decode_bytes(cdata))
  671. if rng.random() < .5:
  672. cdata = cdata.decode("ascii")
  673. rdata = rdata.decode("ascii")
  674. if cdata == rdata:
  675. # should leave unchanged
  676. ok, result = check_repair_unused(cdata)
  677. self.assertFalse(ok)
  678. self.assertEqual(result, rdata)
  679. else:
  680. # should repair bits
  681. self.assertNotEqual(size % 4, 0)
  682. ok, result = check_repair_unused(cdata)
  683. self.assertTrue(ok)
  684. self.assertEqual(result, rdata)
  685. i += 1
  686. #===================================================================
  687. # test transposed encode/decode - encoding independant
  688. #===================================================================
  689. # NOTE: these tests assume normal encode/decode has been tested elsewhere.
  690. transposed = [
  691. # orig, result, transpose map
  692. (b"\x33\x22\x11", b"\x11\x22\x33",[2,1,0]),
  693. (b"\x22\x33\x11", b"\x11\x22\x33",[1,2,0]),
  694. ]
  695. transposed_dups = [
  696. # orig, result, transpose projection
  697. (b"\x11\x11\x22", b"\x11\x22\x33",[0,0,1]),
  698. ]
  699. def test_encode_transposed_bytes(self):
  700. """test encode_transposed_bytes()"""
  701. engine = self.engine
  702. for result, input, offsets in self.transposed + self.transposed_dups:
  703. tmp = engine.encode_transposed_bytes(input, offsets)
  704. out = engine.decode_bytes(tmp)
  705. self.assertEqual(out, result)
  706. self.assertRaises(TypeError, engine.encode_transposed_bytes, u("a"), [])
  707. def test_decode_transposed_bytes(self):
  708. """test decode_transposed_bytes()"""
  709. engine = self.engine
  710. for input, result, offsets in self.transposed:
  711. tmp = engine.encode_bytes(input)
  712. out = engine.decode_transposed_bytes(tmp, offsets)
  713. self.assertEqual(out, result)
  714. def test_decode_transposed_bytes_bad(self):
  715. """test decode_transposed_bytes() fails if map is a one-way"""
  716. engine = self.engine
  717. for input, _, offsets in self.transposed_dups:
  718. tmp = engine.encode_bytes(input)
  719. self.assertRaises(TypeError, engine.decode_transposed_bytes, tmp,
  720. offsets)
  721. #===================================================================
  722. # test 6bit handling
  723. #===================================================================
  724. def check_int_pair(self, bits, encoded_pairs):
  725. """helper to check encode_intXX & decode_intXX functions"""
  726. rng = self.getRandom()
  727. engine = self.engine
  728. encode = getattr(engine, "encode_int%s" % bits)
  729. decode = getattr(engine, "decode_int%s" % bits)
  730. pad = -bits % 6
  731. chars = (bits+pad)//6
  732. upper = 1<<bits
  733. # test encode func
  734. for value, encoded in encoded_pairs:
  735. result = encode(value)
  736. self.assertIsInstance(result, bytes)
  737. self.assertEqual(result, encoded)
  738. self.assertRaises(ValueError, encode, -1)
  739. self.assertRaises(ValueError, encode, upper)
  740. # test decode func
  741. for value, encoded in encoded_pairs:
  742. self.assertEqual(decode(encoded), value, "encoded %r:" % (encoded,))
  743. m = self.m
  744. self.assertRaises(ValueError, decode, m(0)*(chars+1))
  745. self.assertRaises(ValueError, decode, m(0)*(chars-1))
  746. self.assertRaises(ValueError, decode, self.bad_byte*chars)
  747. self.assertRaises(TypeError, decode, engine.charmap[0])
  748. self.assertRaises(TypeError, decode, None)
  749. # do random testing.
  750. from passlib.utils import getrandstr
  751. for i in irange(100):
  752. # generate random value, encode, and then decode
  753. value = rng.randint(0, upper-1)
  754. encoded = encode(value)
  755. self.assertEqual(len(encoded), chars)
  756. self.assertEqual(decode(encoded), value)
  757. # generate some random encoded data, decode, then encode.
  758. encoded = getrandstr(rng, engine.bytemap, chars)
  759. value = decode(encoded)
  760. self.assertGreaterEqual(value, 0, "decode %r out of bounds:" % encoded)
  761. self.assertLess(value, upper, "decode %r out of bounds:" % encoded)
  762. result = encode(value)
  763. if pad:
  764. self.assertEqual(result[:-2], encoded[:-2])
  765. else:
  766. self.assertEqual(result, encoded)
  767. def test_int6(self):
  768. engine = self.engine
  769. m = self.m
  770. self.check_int_pair(6, [(0, m(0)), (63, m(63))])
  771. def test_int12(self):
  772. engine = self.engine
  773. m = self.m
  774. self.check_int_pair(12,[(0, m(0,0)),
  775. (63, m(0,63) if engine.big else m(63,0)), (0xFFF, m(63,63))])
  776. def test_int24(self):
  777. engine = self.engine
  778. m = self.m
  779. self.check_int_pair(24,[(0, m(0,0,0,0)),
  780. (63, m(0,0,0,63) if engine.big else m(63,0,0,0)),
  781. (0xFFFFFF, m(63,63,63,63))])
  782. def test_int64(self):
  783. # NOTE: this isn't multiple of 6, it has 2 padding bits appended
  784. # before encoding.
  785. engine = self.engine
  786. m = self.m
  787. self.check_int_pair(64, [(0, m(0,0,0,0, 0,0,0,0, 0,0,0)),
  788. (63, m(0,0,0,0, 0,0,0,0, 0,3,60) if engine.big else
  789. m(63,0,0,0, 0,0,0,0, 0,0,0)),
  790. ((1<<64)-1, m(63,63,63,63, 63,63,63,63, 63,63,60) if engine.big
  791. else m(63,63,63,63, 63,63,63,63, 63,63,15))])
  792. def test_encoded_ints(self):
  793. """test against reference integer encodings"""
  794. if not self.encoded_ints:
  795. raise self.skipTests("none defined for class")
  796. engine = self.engine
  797. for data, value, bits in self.encoded_ints:
  798. encode = getattr(engine, "encode_int%d" % bits)
  799. decode = getattr(engine, "decode_int%d" % bits)
  800. self.assertEqual(encode(value), data)
  801. self.assertEqual(decode(data), value)
  802. #===================================================================
  803. # eoc
  804. #===================================================================
  805. # NOTE: testing H64 & H64Big should be sufficient to verify
  806. # that Base64Engine() works in general.
  807. from passlib.utils.binary import h64, h64big
  808. class H64_Test(_Base64Test):
  809. """test H64 codec functions"""
  810. engine = h64
  811. descriptionPrefix = "h64 codec"
  812. encoded_data = [
  813. # test lengths 0..6 to ensure tail is encoded properly
  814. (b"",b""),
  815. (b"\x55",b"J/"),
  816. (b"\x55\xaa",b"Jd8"),
  817. (b"\x55\xaa\x55",b"JdOJ"),
  818. (b"\x55\xaa\x55\xaa",b"JdOJe0"),
  819. (b"\x55\xaa\x55\xaa\x55",b"JdOJeK3"),
  820. (b"\x55\xaa\x55\xaa\x55\xaa",b"JdOJeKZe"),
  821. # test padding bits are null
  822. (b"\x55\xaa\x55\xaf",b"JdOJj0"), # len = 1 mod 3
  823. (b"\x55\xaa\x55\xaa\x5f",b"JdOJey3"), # len = 2 mod 3
  824. ]
  825. encoded_ints = [
  826. (b"z.", 63, 12),
  827. (b".z", 4032, 12),
  828. ]
  829. class H64Big_Test(_Base64Test):
  830. """test H64Big codec functions"""
  831. engine = h64big
  832. descriptionPrefix = "h64big codec"
  833. encoded_data = [
  834. # test lengths 0..6 to ensure tail is encoded properly
  835. (b"",b""),
  836. (b"\x55",b"JE"),
  837. (b"\x55\xaa",b"JOc"),
  838. (b"\x55\xaa\x55",b"JOdJ"),
  839. (b"\x55\xaa\x55\xaa",b"JOdJeU"),
  840. (b"\x55\xaa\x55\xaa\x55",b"JOdJeZI"),
  841. (b"\x55\xaa\x55\xaa\x55\xaa",b"JOdJeZKe"),
  842. # test padding bits are null
  843. (b"\x55\xaa\x55\xaf",b"JOdJfk"), # len = 1 mod 3
  844. (b"\x55\xaa\x55\xaa\x5f",b"JOdJeZw"), # len = 2 mod 3
  845. ]
  846. encoded_ints = [
  847. (b".z", 63, 12),
  848. (b"z.", 4032, 12),
  849. ]
  850. #=============================================================================
  851. # eof
  852. #=============================================================================