"""tests for passlib.util""" #========================================================= #imports #========================================================= from __future__ import with_statement #core from binascii import hexlify, unhexlify import sys import random import warnings #site #pkg #module from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \ unicode, bjoin from passlib.tests.utils import TestCase, Params as ak, enable_option, catch_all_warnings def hb(source): return unhexlify(b(source)) #========================================================= #byte funcs #========================================================= class MiscTest(TestCase): "tests various parts of utils module" #NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test def test_getrandbytes(self): "test getrandbytes()" from passlib.utils import getrandbytes, rng def f(*a,**k): return getrandbytes(rng, *a, **k) self.assertEqual(len(f(0)), 0) a = f(10) b = f(10) self.assertIsInstance(a, bytes) self.assertEqual(len(a), 10) self.assertEqual(len(b), 10) self.assertNotEqual(a, b) def test_getrandstr(self): "test getrandstr()" from passlib.utils import getrandstr, rng def f(*a,**k): return getrandstr(rng, *a, **k) #count 0 self.assertEqual(f('abc',0), '') #count <0 self.assertRaises(ValueError, f, 'abc', -1) #letters 0 self.assertRaises(ValueError, f, '', 0) #letters 1 self.assertEqual(f('a',5), 'aaaaa') #letters x = f(u('abc'), 16) y = f(u('abc'), 16) self.assertIsInstance(x, unicode) self.assertNotEqual(x,y) self.assertEqual(sorted(set(x)), [u('a'),u('b'),u('c')]) #bytes x = f(b('abc'), 16) y = f(b('abc'), 16) self.assertIsInstance(x, bytes) self.assertNotEqual(x,y) #NOTE: decoding this due to py3 bytes self.assertEqual(sorted(set(x.decode("ascii"))), [u('a'),u('b'),u('c')]) #generate_password from passlib.utils import generate_password self.assertEqual(len(generate_password(15)), 15) def test_is_crypt_context(self): "test is_crypt_context()" from passlib.utils import is_crypt_context from passlib.context import CryptContext cc = CryptContext(["des_crypt"]) self.assertTrue(is_crypt_context(cc)) self.assertFalse(not is_crypt_context(cc)) def test_genseed(self): "test genseed()" import random from passlib.utils import genseed rng = random.Random(genseed()) a = rng.randint(0, 100000) rng = random.Random(genseed()) b = rng.randint(0, 100000) self.assertNotEqual(a,b) rng.seed(genseed(rng)) def test_crypt(self): "test crypt.crypt() wrappers" from passlib.utils import has_crypt, safe_crypt, test_crypt # test everything is disabled if not has_crypt: self.assertEqual(safe_crypt("test", "aa"), None) self.assertFalse(test_crypt("test", "aaqPiZY5xR5l.")) raise self.skipTest("crypt.crypt() not available") # XXX: this assumes *every* crypt() implementation supports des_crypt. # if this fails for some platform, this test will need modifying. # test return type self.assertIsInstance(safe_crypt(u("test"), u("aa")), unicode) # test ascii password h1 = u('aaqPiZY5xR5l.') self.assertEqual(safe_crypt(u('test'), u('aa')), h1) self.assertEqual(safe_crypt(b('test'), b('aa')), h1) # test utf-8 / unicode password h2 = u('aahWwbrUsKZk.') self.assertEqual(safe_crypt(u('test\u1234'), 'aa'), h2) self.assertEqual(safe_crypt(b('test\xe1\x88\xb4'), 'aa'), h2) # test latin-1 password hash = safe_crypt(b('test\xff'), 'aa') if PY3: # py3 supports utf-8 bytes only. self.assertEqual(hash, None) else: # but py2 is fine. self.assertEqual(hash, u('aaOx.5nbTU/.M')) # test rejects null chars in password self.assertRaises(ValueError, safe_crypt, '\x00', 'aa') # check test_crypt() h1x = h1[:-1] + 'x' self.assertTrue(test_crypt("test", h1)) self.assertFalse(test_crypt("test", h1x)) # check crypt returning variant error indicators # some platforms return None on errors, others empty string, # The BSDs in some cases return ":" import passlib.utils as mod orig = mod._crypt try: fake = None mod._crypt = lambda secret, hash: fake for fake in [None, "", ":", ":0", "*0"]: self.assertEqual(safe_crypt("test", "aa"), None) self.assertFalse(test_crypt("test", h1)) fake = 'xxx' self.assertEqual(safe_crypt("test", "aa"), "xxx") finally: mod._crypt = orig def test_consteq(self): "test consteq()" # NOTE: this test is kind of over the top, but that's only because # this is used for the critical task of comparing hashes for equality. from passlib.utils import consteq # ensure error raises for wrong types self.assertRaises(TypeError, consteq, u(''), b('')) self.assertRaises(TypeError, consteq, u(''), 1) self.assertRaises(TypeError, consteq, u(''), None) self.assertRaises(TypeError, consteq, b(''), u('')) self.assertRaises(TypeError, consteq, b(''), 1) self.assertRaises(TypeError, consteq, b(''), None) self.assertRaises(TypeError, consteq, None, u('')) self.assertRaises(TypeError, consteq, None, b('')) self.assertRaises(TypeError, consteq, 1, u('')) self.assertRaises(TypeError, consteq, 1, b('')) # check equal inputs compare correctly for value in [ u("a"), u("abc"), u("\xff\xa2\x12\x00")*10, ]: self.assertTrue(consteq(value, value), "value %r:" % (value,)) value = value.encode("latin-1") self.assertTrue(consteq(value, value), "value %r:" % (value,)) # check non-equal inputs compare correctly for l,r in [ # check same-size comparisons with differing contents fail. (u("a"), u("c")), (u("abcabc"), u("zbaabc")), (u("abcabc"), u("abzabc")), (u("abcabc"), u("abcabz")), ((u("\xff\xa2\x12\x00")*10)[:-1] + u("\x01"), u("\xff\xa2\x12\x00")*10), # check different-size comparisons fail. (u(""), u("a")), (u("abc"), u("abcdef")), (u("abc"), u("defabc")), (u("qwertyuiopasdfghjklzxcvbnm"), u("abc")), ]: self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) l = l.encode("latin-1") r = r.encode("latin-1") self.assertFalse(consteq(l, r), "values %r %r:" % (l,r)) self.assertFalse(consteq(r, l), "values %r %r:" % (r,l)) # TODO: add some tests to ensure we take THETA(strlen) time. # this might be hard to do reproducably. # NOTE: below code was used to generate stats for analysis ##from math import log as logb ##import timeit ##multipliers = [ 1< encode() -> decode() -> raw # # generate some random bytes size = random.randint(1 if saw_zero else 0, 12) if not size: saw_zero = True enc_size = (4*size+2)//3 raw = getrandbytes(random, size) # encode them, check invariants encoded = engine.encode_bytes(raw) self.assertEqual(len(encoded), enc_size) # make sure decode returns original result = engine.decode_bytes(encoded) self.assertEqual(result, raw) # # test encoded -> decode() -> encode() -> encoded # # generate some random encoded data if size % 4 == 1: size += random.choice([-1,1,2]) raw_size = 3*size//4 encoded = getrandstr(random, engine.bytemap, size) # decode them, check invariants raw = engine.decode_bytes(encoded) self.assertEqual(len(raw), raw_size, "encoded %d:" % size) # make sure encode returns original (barring padding bits) result = engine.encode_bytes(raw) if size % 4: self.assertEqual(result[:-1], encoded[:-1]) else: self.assertEqual(result, encoded) def test_repair_unused(self): "test repair_unused()" # NOTE: this test relies on encode_bytes() always returning clear # padding bits - which should be ensured by test vectors. from passlib.utils import rng, getrandstr engine = self.engine check_repair_unused = self.engine.check_repair_unused i = 0 while i < 300: size = rng.randint(0,23) cdata = getrandstr(rng, engine.charmap, size).encode("ascii") if size & 3 == 1: # should throw error self.assertRaises(ValueError, check_repair_unused, cdata) continue rdata = engine.encode_bytes(engine.decode_bytes(cdata)) if rng.random() < .5: cdata = cdata.decode("ascii") rdata = rdata.decode("ascii") if cdata == rdata: # should leave unchanged ok, result = check_repair_unused(cdata) self.assertFalse(ok) self.assertEqual(result, rdata) else: # should repair bits self.assertNotEqual(size % 4, 0) ok, result = check_repair_unused(cdata) self.assertTrue(ok) self.assertEqual(result, rdata) i += 1 #========================================================= # test transposed encode/decode - encoding independant #========================================================= # NOTE: these tests assume normal encode/decode has been tested elsewhere. transposed = [ # orig, result, transpose map (b("\x33\x22\x11"), b("\x11\x22\x33"),[2,1,0]), (b("\x22\x33\x11"), b("\x11\x22\x33"),[1,2,0]), ] transposed_dups = [ # orig, result, transpose projection (b("\x11\x11\x22"), b("\x11\x22\x33"),[0,0,1]), ] def test_encode_transposed_bytes(self): "test encode_transposed_bytes()" engine = self.engine for result, input, offsets in self.transposed + self.transposed_dups: tmp = engine.encode_transposed_bytes(input, offsets) out = engine.decode_bytes(tmp) self.assertEqual(out, result) def test_decode_transposed_bytes(self): "test decode_transposed_bytes()" engine = self.engine for input, result, offsets in self.transposed: tmp = engine.encode_bytes(input) out = engine.decode_transposed_bytes(tmp, offsets) self.assertEqual(out, result) def test_decode_transposed_bytes_bad(self): "test decode_transposed_bytes() fails if map is a one-way" engine = self.engine for input, _, offsets in self.transposed_dups: tmp = engine.encode_bytes(input) self.assertRaises(TypeError, engine.decode_transposed_bytes, tmp, offsets) #========================================================= # test 6bit handling #========================================================= def check_int_pair(self, bits, encoded_pairs): "helper to check encode_intXX & decode_intXX functions" engine = self.engine encode = getattr(engine, "encode_int%s" % bits) decode = getattr(engine, "decode_int%s" % bits) pad = -bits % 6 chars = (bits+pad)//6 upper = 1< hex digest (b(""), "31d6cfe0d16ae931b73c59d7e0c089c0"), (b("a"), "bde52cb31de33e46245e05fbdbd6fb24"), (b("abc"), "a448017aaf21d8525fc10ae87aa6729d"), (b("message digest"), "d9130a8164549fe818874806e1c7014b"), (b("abcdefghijklmnopqrstuvwxyz"), "d79e1c308aa5bbcdeea8ed63df412da9"), (b("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"), "043f8582f241db351ce627e153e7f0e4"), (b("12345678901234567890123456789012345678901234567890123456789012345678901234567890"), "e33b4ddc9c38f2199c3e7b164fcc0536"), ] def test_md4_update(self): "test md4 update" md4 = self.hash h = md4(b('')) self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0") #NOTE: under py2, hashlib methods try to encode to ascii, # though shouldn't rely on that. if PY3: self.assertRaises(TypeError, h.update, u('x')) h.update(b('a')) self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24") h.update(b('bcdefghijklmnopqrstuvwxyz')) self.assertEqual(h.hexdigest(), "d79e1c308aa5bbcdeea8ed63df412da9") def test_md4_hexdigest(self): "test md4 hexdigest()" md4 = self.hash for input, hex in self.vectors: out = md4(input).hexdigest() self.assertEqual(out, hex) def test_md4_digest(self): "test md4 digest()" md4 = self.hash for input, hex in self.vectors: out = bascii_to_str(hexlify(md4(input).digest())) self.assertEqual(out, hex) def test_md4_copy(self): "test md4 copy()" md4 = self.hash h = md4(b('abc')) h2 = h.copy() h2.update(b('def')) self.assertEqual(h2.hexdigest(), '804e7f1c2586e50b49ac65db5b645131') h.update(b('ghi')) self.assertEqual(h.hexdigest(), 'c5225580bfe176f6deeee33dee98732c') # #now do a bunch of things to test multiple possible backends. # import passlib.utils.md4 as md4_mod has_ssl_md4 = (md4_mod.md4 is not md4_mod._builtin_md4) if has_ssl_md4: class MD4_SSL_Test(_MD4_Test): case_prefix = "MD4 (SSL version)" hash = staticmethod(md4_mod.md4) if not has_ssl_md4 or enable_option("cover"): class MD4_Builtin_Test(_MD4_Test): case_prefix = "MD4 (builtin version)" hash = md4_mod._builtin_md4 #========================================================= #test passlib.utils.pbkdf2 #========================================================= import hashlib import hmac from passlib.utils import pbkdf2 #TODO: should we bother testing hmac_sha1() function? it's verified via sha1_crypt testing. class CryptoTest(TestCase): "test various crypto functions" ndn_formats = ["hashlib", "iana"] ndn_values = [ # (iana name, hashlib name, ... other unnormalized names) ("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"), ("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"), ("sha256", "sha-256", "SHA_256", "sha2-256"), ("ripemd", "ripemd", "SCRAM-RIPEMD", "RIPEMD"), ("ripemd160", "ripemd-160", "SCRAM-RIPEMD-160", "RIPEmd160"), ("test128", "test-128", "TEST128"), ("test2", "test2", "TEST-2"), ("test3128", "test3-128", "TEST-3-128"), ] def test_norm_hash_name(self): "test norm_hash_name()" from itertools import chain from passlib.utils.pbkdf2 import norm_hash_name, _nhn_hash_names # test formats for format in self.ndn_formats: norm_hash_name("md4", format) self.assertRaises(ValueError, norm_hash_name, "md4", None) self.assertRaises(ValueError, norm_hash_name, "md4", "fake") # test types self.assertEqual(norm_hash_name(u("MD4")), "md4") self.assertEqual(norm_hash_name(b("MD4")), "md4") self.assertRaises(TypeError, norm_hash_name, None) # test selected results with catch_all_warnings(): warnings.filterwarnings("ignore", '.*unknown hash') for row in chain(_nhn_hash_names, self.ndn_values): for idx, format in enumerate(self.ndn_formats): correct = row[idx] for value in row: result = norm_hash_name(value, format) self.assertEqual(result, correct, "name=%r, format=%r:" % (value, format)) class KdfTest(TestCase): "test kdf helpers" def test_pbkdf1(self): "test pbkdf1" for secret, salt, rounds, klen, hash, correct in [ #http://www.di-mgt.com.au/cryptoKDFs.html (b('password'), hb('78578E5A5D63CB06'), 1000, 16, 'sha1', hb('dc19847e05c64d2faf10ebfb4a3d2a20')), ]: result = pbkdf2.pbkdf1(secret, salt, rounds, klen, hash) self.assertEqual(result, correct) #test rounds < 1 #test klen < 0 #test klen > block size #test invalid hash #NOTE: this is not run directly, but via two subclasses (below) class _Pbkdf2BackendTest(TestCase): "test builtin unix crypt backend" enable_m2crypto = False def setUp(self): #disable m2crypto support so we'll always use software backend if not self.enable_m2crypto: self._orig_EVP = pbkdf2._EVP pbkdf2._EVP = None else: #set flag so tests can check for m2crypto presence quickly self.enable_m2crypto = bool(pbkdf2._EVP) pbkdf2._clear_prf_cache() def tearDown(self): if not self.enable_m2crypto: pbkdf2._EVP = self._orig_EVP pbkdf2._clear_prf_cache() #TODO: test get_prf() behavior in various situations - though overall behavior tested via pbkdf2 def test_rfc3962(self): "rfc3962 test vectors" self.assertFunctionResults(pbkdf2.pbkdf2, [ # result, secret, salt, rounds, keylen, digest="sha1" #test case 1 / 128 bit ( hb("cdedb5281bb2f801565a1122b2563515"), b("password"), b("ATHENA.MIT.EDUraeburn"), 1, 16 ), #test case 2 / 128 bit ( hb("01dbee7f4a9e243e988b62c73cda935d"), b("password"), b("ATHENA.MIT.EDUraeburn"), 2, 16 ), #test case 2 / 256 bit ( hb("01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86"), b("password"), b("ATHENA.MIT.EDUraeburn"), 2, 32 ), #test case 3 / 256 bit ( hb("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13"), b("password"), b("ATHENA.MIT.EDUraeburn"), 1200, 32 ), #test case 4 / 256 bit ( hb("d1daa78615f287e6a1c8b120d7062a493f98d203e6be49a6adf4fa574b6e64ee"), b("password"), b('\x12\x34\x56\x78\x78\x56\x34\x12'), 5, 32 ), #test case 5 / 256 bit ( hb("139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1"), b("X"*64), b("pass phrase equals block size"), 1200, 32 ), #test case 6 / 256 bit ( hb("9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a"), b("X"*65), b("pass phrase exceeds block size"), 1200, 32 ), ]) def test_rfc6070(self): "rfc6070 test vectors" self.assertFunctionResults(pbkdf2.pbkdf2, [ ( hb("0c60c80f961f0e71f3a9b524af6012062fe037a6"), b("password"), b("salt"), 1, 20, ), ( hb("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"), b("password"), b("salt"), 2, 20, ), ( hb("4b007901b765489abead49d926f721d065a429c1"), b("password"), b("salt"), 4096, 20, ), #just runs too long - could enable if ALL option is set ##( ## ## unhexlify("eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"), ## "password", "salt", 16777216, 20, ##), ( hb("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"), b("passwordPASSWORDpassword"), b("saltSALTsaltSALTsaltSALTsaltSALTsalt"), 4096, 25, ), ( hb("56fa6aa75548099dcc37d7f03425e0c3"), b("pass\00word"), b("sa\00lt"), 4096, 16, ), ]) def test_invalid_values(self): #invalid rounds self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), -1, 16) self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 0, 16) self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), b('salt'), 'x', 16) #invalid keylen self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 20*(2**32-1)+1) #invalid salt type self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), 5, 1, 10) #invalid secret type self.assertRaises(TypeError, pbkdf2.pbkdf2, 5, b('salt'), 1, 10) #invalid hash self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 'hmac-foo') self.assertRaises(ValueError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 'foo') self.assertRaises(TypeError, pbkdf2.pbkdf2, b('password'), b('salt'), 1, 16, 5) def test_default_keylen(self): "test keylen==-1" self.assertEqual(len(pbkdf2.pbkdf2(b('password'), b('salt'), 1, -1, prf='hmac-sha1')), 20) self.assertEqual(len(pbkdf2.pbkdf2(b('password'), b('salt'), 1, -1, prf='hmac-sha256')), 32) def test_hmac_sha1(self): "test independant hmac_sha1() method" self.assertEqual( pbkdf2.hmac_sha1(b("secret"), b("salt")), b('\xfc\xd4\x0c;]\r\x97\xc6\xf1S\x8d\x93\xb9\xeb\xc6\x00\x04.\x8b\xfe') ) def test_sha1_string(self): "test various prf values" self.assertEqual( pbkdf2.pbkdf2(b("secret"), b("salt"), 10, 16, "hmac-sha1"), b('\xe2H\xfbk\x136QF\xf8\xacc\x07\xcc"(\x12') ) def test_sha512_string(self): "test alternate digest string (sha512)" self.assertFunctionResults(pbkdf2.pbkdf2, [ # result, secret, salt, rounds, keylen, digest="sha1" #case taken from example in http://grub.enbug.org/Authentication ( hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC6C29E293F0A0"), b("hello"), hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073994D79080136"), 10000, 64, "hmac-sha512" ), ]) def test_sha512_function(self): "test custom digest function" def prf(key, msg): return hmac.new(key, msg, hashlib.sha512).digest() self.assertFunctionResults(pbkdf2.pbkdf2, [ # result, secret, salt, rounds, keylen, digest="sha1" #case taken from example in http://grub.enbug.org/Authentication ( hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC6C29E293F0A0"), b("hello"), hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073994D79080136"), 10000, 64, prf, ), ]) has_m2crypto = (pbkdf2._EVP is not None) if has_m2crypto: class Pbkdf2_M2Crypto_Test(_Pbkdf2BackendTest): case_prefix = "pbkdf2 (m2crypto backend)" enable_m2crypto = True if not has_m2crypto or enable_option("cover"): class Pbkdf2_Builtin_Test(_Pbkdf2BackendTest): case_prefix = "pbkdf2 (builtin backend)" enable_m2crypto = False #========================================================= #EOF #=========================================================