Загрузить файлы в «venv/Lib/site-packages/passlib/tests»

This commit is contained in:
2026-07-02 18:51:47 +00:00
parent 8b5969dcff
commit ac6973375d
5 changed files with 4290 additions and 0 deletions

View File

@@ -0,0 +1,634 @@
"""tests for passlib.utils.scrypt"""
#=============================================================================
# imports
#=============================================================================
# core
from binascii import hexlify
import hashlib
import logging; log = logging.getLogger(__name__)
import struct
import warnings
warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*")
# site
# pkg
from passlib import exc
from passlib.utils import getrandbytes
from passlib.utils.compat import PYPY, u, bascii_to_str
from passlib.utils.decor import classproperty
from passlib.tests.utils import TestCase, skipUnless, TEST_MODE, hb
# subject
from passlib.crypto import scrypt as scrypt_mod
# local
__all__ = [
"ScryptEngineTest",
"BuiltinScryptTest",
"FastScryptTest",
]
#=============================================================================
# support functions
#=============================================================================
def hexstr(data):
"""return bytes as hex str"""
return bascii_to_str(hexlify(data))
def unpack_uint32_list(data, check_count=None):
"""unpack bytes as list of uint32 values"""
count = len(data) // 4
assert check_count is None or check_count == count
return struct.unpack("<%dI" % count, data)
def seed_bytes(seed, count):
"""
generate random reference bytes from specified seed.
used to generate some predictable test vectors.
"""
if hasattr(seed, "encode"):
seed = seed.encode("ascii")
buf = b''
i = 0
while len(buf) < count:
buf += hashlib.sha256(seed + struct.pack("<I", i)).digest()
i += 1
return buf[:count]
#=============================================================================
# test builtin engine's internals
#=============================================================================
class ScryptEngineTest(TestCase):
descriptionPrefix = "passlib.crypto.scrypt._builtin"
def test_smix(self):
"""smix()"""
from passlib.crypto.scrypt._builtin import ScryptEngine
rng = self.getRandom()
#-----------------------------------------------------------------------
# test vector from (expired) scrypt rfc draft
# (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 9)
#-----------------------------------------------------------------------
input = hb("""
f7 ce 0b 65 3d 2d 72 a4 10 8c f5 ab e9 12 ff dd
77 76 16 db bb 27 a7 0e 82 04 f3 ae 2d 0f 6f ad
89 f6 8f 48 11 d1 e8 7b cc 3b d7 40 0a 9f fd 29
09 4f 01 84 63 95 74 f3 9a e5 a1 31 52 17 bc d7
89 49 91 44 72 13 bb 22 6c 25 b5 4d a8 63 70 fb
cd 98 43 80 37 46 66 bb 8f fc b5 bf 40 c2 54 b0
67 d2 7c 51 ce 4a d5 fe d8 29 c9 0b 50 5a 57 1b
7f 4d 1c ad 6a 52 3c da 77 0e 67 bc ea af 7e 89
""")
output = hb("""
79 cc c1 93 62 9d eb ca 04 7f 0b 70 60 4b f6 b6
2c e3 dd 4a 96 26 e3 55 fa fc 61 98 e6 ea 2b 46
d5 84 13 67 3b 99 b0 29 d6 65 c3 57 60 1f b4 26
a0 b2 f4 bb a2 00 ee 9f 0a 43 d1 9b 57 1a 9c 71
ef 11 42 e6 5d 5a 26 6f dd ca 83 2c e5 9f aa 7c
ac 0b 9c f1 be 2b ff ca 30 0d 01 ee 38 76 19 c4
ae 12 fd 44 38 f2 03 a0 e4 e1 c4 7e c3 14 86 1f
4e 90 87 cb 33 39 6a 68 73 e8 f9 d2 53 9a 4b 8e
""")
# NOTE: p value should be ignored, so testing w/ random inputs.
engine = ScryptEngine(n=16, r=1, p=rng.randint(1, 1023))
self.assertEqual(engine.smix(input), output)
def test_bmix(self):
"""bmix()"""
from passlib.crypto.scrypt._builtin import ScryptEngine
rng = self.getRandom()
# NOTE: bmix() call signature currently takes in list of 32*r uint32 elements,
# and writes to target buffer of same size.
def check_bmix(r, input, output):
"""helper to check bmix() output against reference"""
# NOTE: * n & p values should be ignored, so testing w/ rng inputs.
# * target buffer contents should be ignored, so testing w/ random inputs.
engine = ScryptEngine(r=r, n=1 << rng.randint(1, 32), p=rng.randint(1, 1023))
target = [rng.randint(0, 1 << 32) for _ in range((2 * r) * 16)]
engine.bmix(input, target)
self.assertEqual(target, list(output))
# ScryptEngine special-cases bmix() for r=1.
# this removes the special case patching, so we also test original bmix function.
if r == 1:
del engine.bmix
target = [rng.randint(0, 1 << 32) for _ in range((2 * r) * 16)]
engine.bmix(input, target)
self.assertEqual(target, list(output))
#-----------------------------------------------------------------------
# test vector from (expired) scrypt rfc draft
# (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 8)
#-----------------------------------------------------------------------
# NOTE: this pair corresponds to the first input & output pair
# from the test vector in test_smix(), above.
# NOTE: original reference lists input & output as two separate 64 byte blocks.
# current internal representation used by bmix() uses single 2*r*16 array of uint32,
# combining all the B blocks into a single flat array.
input = unpack_uint32_list(hb("""
f7 ce 0b 65 3d 2d 72 a4 10 8c f5 ab e9 12 ff dd
77 76 16 db bb 27 a7 0e 82 04 f3 ae 2d 0f 6f ad
89 f6 8f 48 11 d1 e8 7b cc 3b d7 40 0a 9f fd 29
09 4f 01 84 63 95 74 f3 9a e5 a1 31 52 17 bc d7
89 49 91 44 72 13 bb 22 6c 25 b5 4d a8 63 70 fb
cd 98 43 80 37 46 66 bb 8f fc b5 bf 40 c2 54 b0
67 d2 7c 51 ce 4a d5 fe d8 29 c9 0b 50 5a 57 1b
7f 4d 1c ad 6a 52 3c da 77 0e 67 bc ea af 7e 89
"""), 32)
output = unpack_uint32_list(hb("""
a4 1f 85 9c 66 08 cc 99 3b 81 ca cb 02 0c ef 05
04 4b 21 81 a2 fd 33 7d fd 7b 1c 63 96 68 2f 29
b4 39 31 68 e3 c9 e6 bc fe 6b c5 b7 a0 6d 96 ba
e4 24 cc 10 2c 91 74 5c 24 ad 67 3d c7 61 8f 81
20 ed c9 75 32 38 81 a8 05 40 f6 4c 16 2d cd 3c
21 07 7c fe 5f 8d 5f e2 b1 a4 16 8f 95 36 78 b7
7d 3b 3d 80 3b 60 e4 ab 92 09 96 e5 9b 4d 53 b6
5d 2a 22 58 77 d5 ed f5 84 2c b9 f1 4e ef e4 25
"""), 32)
# check_bmix(1, input, output)
#-----------------------------------------------------------------------
# custom test vector for r=2
# used to check for bmix() breakage while optimizing implementation.
#-----------------------------------------------------------------------
r = 2
input = unpack_uint32_list(seed_bytes("bmix with r=2", 128 * r))
output = unpack_uint32_list(hb("""
ba240854954f4585f3d0573321f10beee96f12acdc1feb498131e40512934fd7
43e8139c17d0743c89d09ac8c3582c273c60ab85db63e410d049a9e17a42c6a1
6c7831b11bf370266afdaff997ae1286920dea1dedf0f4a1795ba710ba9017f1
a374400766f13ebd8969362de2d153965e9941bdde0768fa5b53e8522f116ce0
d14774afb88f46cd919cba4bc64af7fca0ecb8732d1fc2191e0d7d1b6475cb2e
e3db789ee478d056c4eb6c6e28b99043602dbb8dfb60c6e048bf90719da8d57d
3c42250e40ab79a1ada6aae9299b9790f767f54f388d024a1465b30cbbe9eb89
002d4f5c215c4259fac4d083bac5fb0b47463747d568f40bb7fa87c42f0a1dc1
"""), 32 * r)
check_bmix(r, input, output)
#-----------------------------------------------------------------------
# custom test vector for r=3
# used to check for bmix() breakage while optimizing implementation.
#-----------------------------------------------------------------------
r = 3
input = unpack_uint32_list(seed_bytes("bmix with r=3", 128 * r))
output = unpack_uint32_list(hb("""
11ddd8cf60c61f59a6e5b128239bdc77b464101312c88bd1ccf6be6e75461b29
7370d4770c904d0b09c402573cf409bf2db47b91ba87d5a3de469df8fb7a003c
95a66af96dbdd88beddc8df51a2f72a6f588d67e7926e9c2b676c875da13161e
b6262adac39e6b3003e9a6fbc8c1a6ecf1e227c03bc0af3e5f8736c339b14f84
c7ae5b89f5e16d0faf8983551165f4bb712d97e4f81426e6b78eb63892d3ff54
80bf406c98e479496d0f76d23d728e67d2a3d2cdbc4a932be6db36dc37c60209
a5ca76ca2d2979f995f73fe8182eefa1ce0ba0d4fc27d5b827cb8e67edd6552f
00a5b3ab6b371bd985a158e728011314eb77f32ade619b3162d7b5078a19886c
06f12bc8ae8afa46489e5b0239954d5216967c928982984101e4a88bae1f60ae
3f8a456e169a8a1c7450e7955b8a13a202382ae19d41ce8ef8b6a15eeef569a7
20f54c48e44cb5543dda032c1a50d5ddf2919030624978704eb8db0290052a1f
5d88989b0ef931b6befcc09e9d5162320e71e80b89862de7e2f0b6c67229b93f
"""), 32 * r)
check_bmix(r, input, output)
#-----------------------------------------------------------------------
# custom test vector for r=4
# used to check for bmix() breakage while optimizing implementation.
#-----------------------------------------------------------------------
r = 4
input = unpack_uint32_list(seed_bytes("bmix with r=4", 128 * r))
output = unpack_uint32_list(hb("""
803fcf7362702f30ef43250f20bc6b1b8925bf5c4a0f5a14bbfd90edce545997
3047bd81655f72588ca93f5c2f4128adaea805e0705a35e14417101fdb1c498c
33bec6f4e5950d66098da8469f3fe633f9a17617c0ea21275185697c0e4608f7
e6b38b7ec71704a810424637e2c296ca30d9cbf8172a71a266e0393deccf98eb
abc430d5f144eb0805308c38522f2973b7b6a48498851e4c762874497da76b88
b769b471fbfc144c0e8e859b2b3f5a11f51604d268c8fd28db55dff79832741a
1ac0dfdaff10f0ada0d93d3b1f13062e4107c640c51df05f4110bdda15f51b53
3a75bfe56489a6d8463440c78fb8c0794135e38591bdc5fa6cec96a124178a4a
d1a976e985bfe13d2b4af51bd0fc36dd4cfc3af08efe033b2323a235205dc43d
e57778a492153f9527338b3f6f5493a03d8015cd69737ee5096ad4cbe660b10f
b75b1595ddc96e3748f5c9f61fba1ef1f0c51b6ceef8bbfcc34b46088652e6f7
edab61521cbad6e69b77be30c9c97ea04a4af359dafc205c7878cc9a6c5d122f
8d77f3cbe65ab14c3c491ef94ecb3f5d2c2dd13027ea4c3606262bb3c9ce46e7
dc424729dc75f6e8f06096c0ad8ad4d549c42f0cad9b33cb95d10fb3cadba27c
5f4bf0c1ac677c23ba23b64f56afc3546e62d96f96b58d7afc5029f8168cbab4
533fd29fc83c8d2a32b81923992e4938281334e0c3694f0ee56f8ff7df7dc4ae
"""), 32 * r)
check_bmix(r, input, output)
def test_salsa(self):
"""salsa20()"""
from passlib.crypto.scrypt._builtin import salsa20
# NOTE: salsa2() currently operates on lists of 16 uint32 elements,
# which is what unpack_uint32_list(hb(() is for...
#-----------------------------------------------------------------------
# test vector from (expired) scrypt rfc draft
# (https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 7)
#-----------------------------------------------------------------------
# NOTE: this pair corresponds to the first input & output pair
# from the test vector in test_bmix(), above.
input = unpack_uint32_list(hb("""
7e 87 9a 21 4f 3e c9 86 7c a9 40 e6 41 71 8f 26
ba ee 55 5b 8c 61 c1 b5 0d f8 46 11 6d cd 3b 1d
ee 24 f3 19 df 9b 3d 85 14 12 1e 4b 5a c5 aa 32
76 02 1d 29 09 c7 48 29 ed eb c6 8d b8 b8 c2 5e
"""))
output = unpack_uint32_list(hb("""
a4 1f 85 9c 66 08 cc 99 3b 81 ca cb 02 0c ef 05
04 4b 21 81 a2 fd 33 7d fd 7b 1c 63 96 68 2f 29
b4 39 31 68 e3 c9 e6 bc fe 6b c5 b7 a0 6d 96 ba
e4 24 cc 10 2c 91 74 5c 24 ad 67 3d c7 61 8f 81
"""))
self.assertEqual(salsa20(input), output)
#-----------------------------------------------------------------------
# custom test vector,
# used to check for salsa20() breakage while optimizing _gen_files output.
#-----------------------------------------------------------------------
input = list(range(16))
output = unpack_uint32_list(hb("""
f518dd4fb98883e0a87954c05cab867083bb8808552810752285a05822f56c16
9d4a2a0fd2142523d758c60b36411b682d53860514b871d27659042a5afa475d
"""))
self.assertEqual(salsa20(input), output)
#=============================================================================
# eof
#=============================================================================
#=============================================================================
# test scrypt
#=============================================================================
class _CommonScryptTest(TestCase):
"""
base class for testing various scrypt backends against same set of reference vectors.
"""
#=============================================================================
# class attrs
#=============================================================================
@classproperty
def descriptionPrefix(cls):
return "passlib.utils.scrypt.scrypt() <%s backend>" % cls.backend
backend = None
#=============================================================================
# setup
#=============================================================================
def setUp(self):
assert self.backend
scrypt_mod._set_backend(self.backend)
super(_CommonScryptTest, self).setUp()
#=============================================================================
# reference vectors
#=============================================================================
reference_vectors = [
# entry format: (secret, salt, n, r, p, keylen, result)
#------------------------------------------------------------------------
# test vectors from scrypt whitepaper --
# http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b
#
# also present in (expired) scrypt rfc draft --
# https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01, section 11
#------------------------------------------------------------------------
("", "", 16, 1, 1, 64, hb("""
77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97
f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42
fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17
e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06
""")),
("password", "NaCl", 1024, 8, 16, 64, hb("""
fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe
7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62
2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da
c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40
""")),
# NOTE: the following are skipped for all backends unless TEST_MODE="full"
("pleaseletmein", "SodiumChloride", 16384, 8, 1, 64, hb("""
70 23 bd cb 3a fd 73 48 46 1c 06 cd 81 fd 38 eb
fd a8 fb ba 90 4f 8e 3e a9 b5 43 f6 54 5d a1 f2
d5 43 29 55 61 3f 0f cf 62 d4 97 05 24 2a 9a f9
e6 1e 85 dc 0d 65 1e 40 df cf 01 7b 45 57 58 87
""")),
# NOTE: the following are always skipped for the builtin backend,
# (just takes too long to be worth it)
("pleaseletmein", "SodiumChloride", 1048576, 8, 1, 64, hb("""
21 01 cb 9b 6a 51 1a ae ad db be 09 cf 70 f8 81
ec 56 8d 57 4a 2f fd 4d ab e5 ee 98 20 ad aa 47
8e 56 fd 8f 4b a5 d0 9f fa 1c 6d 92 7c 40 f4 c3
37 30 40 49 e8 a9 52 fb cb f4 5c 6f a7 7a 41 a4
""")),
]
def test_reference_vectors(self):
"""reference vectors"""
for secret, salt, n, r, p, keylen, result in self.reference_vectors:
if n >= 1024 and TEST_MODE(max="default"):
# skip large values unless we're running full test suite
continue
if n > 16384 and self.backend == "builtin":
# skip largest vector for builtin, takes WAAY too long
# (46s under pypy, ~5m under cpython)
continue
log.debug("scrypt reference vector: %r %r n=%r r=%r p=%r", secret, salt, n, r, p)
self.assertEqual(scrypt_mod.scrypt(secret, salt, n, r, p, keylen), result)
#=============================================================================
# fuzz testing
#=============================================================================
_already_tested_others = None
def test_other_backends(self):
"""compare output to other backends"""
# only run once, since test is symetric.
# maybe this means it should go somewhere else?
if self._already_tested_others:
raise self.skipTest("already run under %r backend test" % self._already_tested_others)
self._already_tested_others = self.backend
rng = self.getRandom()
# get available backends
orig = scrypt_mod.backend
available = set(name for name in scrypt_mod.backend_values
if scrypt_mod._has_backend(name))
scrypt_mod._set_backend(orig)
available.discard(self.backend)
if not available:
raise self.skipTest("no other backends found")
warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend",
category=exc.PasslibSecurityWarning)
# generate some random options, and cross-check output
for _ in range(10):
# NOTE: keeping values low due to builtin test
secret = getrandbytes(rng, rng.randint(0, 64))
salt = getrandbytes(rng, rng.randint(0, 64))
n = 1 << rng.randint(1, 10)
r = rng.randint(1, 8)
p = rng.randint(1, 3)
ks = rng.randint(1, 64)
previous = None
backends = set()
for name in available:
scrypt_mod._set_backend(name)
self.assertNotIn(scrypt_mod._scrypt, backends)
backends.add(scrypt_mod._scrypt)
result = hexstr(scrypt_mod.scrypt(secret, salt, n, r, p, ks))
self.assertEqual(len(result), 2*ks)
if previous is not None:
self.assertEqual(result, previous,
msg="%r output differs from others %r: %r" %
(name, available, [secret, salt, n, r, p, ks]))
#=============================================================================
# test input types
#=============================================================================
def test_backend(self):
"""backend management"""
# clobber backend
scrypt_mod.backend = None
scrypt_mod._scrypt = None
self.assertRaises(TypeError, scrypt_mod.scrypt, 's', 's', 2, 2, 2, 16)
# reload backend
scrypt_mod._set_backend(self.backend)
self.assertEqual(scrypt_mod.backend, self.backend)
scrypt_mod.scrypt('s', 's', 2, 2, 2, 16)
# throw error for unknown backend
self.assertRaises(ValueError, scrypt_mod._set_backend, 'xxx')
self.assertEqual(scrypt_mod.backend, self.backend)
def test_secret_param(self):
"""'secret' parameter"""
def run_scrypt(secret):
return hexstr(scrypt_mod.scrypt(secret, "salt", 2, 2, 2, 16))
# unicode
TEXT = u("abc\u00defg")
self.assertEqual(run_scrypt(TEXT), '05717106997bfe0da42cf4779a2f8bd8')
# utf8 bytes
TEXT_UTF8 = b'abc\xc3\x9efg'
self.assertEqual(run_scrypt(TEXT_UTF8), '05717106997bfe0da42cf4779a2f8bd8')
# latin1 bytes
TEXT_LATIN1 = b'abc\xdefg'
self.assertEqual(run_scrypt(TEXT_LATIN1), '770825d10eeaaeaf98e8a3c40f9f441d')
# accept empty string
self.assertEqual(run_scrypt(""), 'ca1399e5fae5d3b9578dcd2b1faff6e2')
# reject other types
self.assertRaises(TypeError, run_scrypt, None)
self.assertRaises(TypeError, run_scrypt, 1)
def test_salt_param(self):
"""'salt' parameter"""
def run_scrypt(salt):
return hexstr(scrypt_mod.scrypt("secret", salt, 2, 2, 2, 16))
# unicode
TEXT = u("abc\u00defg")
self.assertEqual(run_scrypt(TEXT), 'a748ec0f4613929e9e5f03d1ab741d88')
# utf8 bytes
TEXT_UTF8 = b'abc\xc3\x9efg'
self.assertEqual(run_scrypt(TEXT_UTF8), 'a748ec0f4613929e9e5f03d1ab741d88')
# latin1 bytes
TEXT_LATIN1 = b'abc\xdefg'
self.assertEqual(run_scrypt(TEXT_LATIN1), '91d056fb76fb6e9a7d1cdfffc0a16cd1')
# reject other types
self.assertRaises(TypeError, run_scrypt, None)
self.assertRaises(TypeError, run_scrypt, 1)
def test_n_param(self):
"""'n' (rounds) parameter"""
def run_scrypt(n):
return hexstr(scrypt_mod.scrypt("secret", "salt", n, 2, 2, 16))
# must be > 1, and a power of 2
self.assertRaises(ValueError, run_scrypt, -1)
self.assertRaises(ValueError, run_scrypt, 0)
self.assertRaises(ValueError, run_scrypt, 1)
self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66')
self.assertRaises(ValueError, run_scrypt, 3)
self.assertRaises(ValueError, run_scrypt, 15)
self.assertEqual(run_scrypt(16), '0272b8fc72bc54b1159340ed99425233')
def test_r_param(self):
"""'r' (block size) parameter"""
def run_scrypt(r, n=2, p=2):
return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16))
# must be > 1
self.assertRaises(ValueError, run_scrypt, -1)
self.assertRaises(ValueError, run_scrypt, 0)
self.assertEqual(run_scrypt(1), '3d630447d9f065363b8a79b0b3670251')
self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66')
self.assertEqual(run_scrypt(5), '114f05e985a903c27237b5578e763736')
# reject r*p >= 2**30
self.assertRaises(ValueError, run_scrypt, (1<<30), p=1)
self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, p=2)
def test_p_param(self):
"""'p' (parallelism) parameter"""
def run_scrypt(p, n=2, r=2):
return hexstr(scrypt_mod.scrypt("secret", "salt", n, r, p, 16))
# must be > 1
self.assertRaises(ValueError, run_scrypt, -1)
self.assertRaises(ValueError, run_scrypt, 0)
self.assertEqual(run_scrypt(1), 'f2960ea8b7d48231fcec1b89b784a6fa')
self.assertEqual(run_scrypt(2), 'dacf2bca255e2870e6636fa8c8957a66')
self.assertEqual(run_scrypt(5), '848a0eeb2b3543e7f543844d6ca79782')
# reject r*p >= 2**30
self.assertRaises(ValueError, run_scrypt, (1<<30), r=1)
self.assertRaises(ValueError, run_scrypt, (1<<30) / 2, r=2)
def test_keylen_param(self):
"""'keylen' parameter"""
rng = self.getRandom()
def run_scrypt(keylen):
return hexstr(scrypt_mod.scrypt("secret", "salt", 2, 2, 2, keylen))
# must be > 0
self.assertRaises(ValueError, run_scrypt, -1)
self.assertRaises(ValueError, run_scrypt, 0)
self.assertEqual(run_scrypt(1), 'da')
# pick random value
ksize = rng.randint(1, 1 << 10)
self.assertEqual(len(run_scrypt(ksize)), 2*ksize) # 2 hex chars per output
# one more than upper bound
self.assertRaises(ValueError, run_scrypt, ((2**32) - 1) * 32 + 1)
#=============================================================================
# eoc
#=============================================================================
#-----------------------------------------------------------------------
# check what backends 'should' be available
#-----------------------------------------------------------------------
def _can_import_cffi_scrypt():
try:
import scrypt
except ImportError as err:
if "scrypt" in str(err):
return False
raise
return True
has_cffi_scrypt = _can_import_cffi_scrypt()
def _can_import_stdlib_scrypt():
try:
from hashlib import scrypt
return True
except ImportError:
return False
has_stdlib_scrypt = _can_import_stdlib_scrypt()
#-----------------------------------------------------------------------
# test individual backends
#-----------------------------------------------------------------------
# NOTE: builtin version runs VERY slow (except under PyPy, where it's only 11x slower),
# so skipping under quick test mode.
@skipUnless(PYPY or TEST_MODE(min="default"), "skipped under current test mode")
class BuiltinScryptTest(_CommonScryptTest):
backend = "builtin"
def setUp(self):
super(BuiltinScryptTest, self).setUp()
warnings.filterwarnings("ignore", "(?i)using builtin scrypt backend",
category=exc.PasslibSecurityWarning)
def test_missing_backend(self):
"""backend management -- missing backend"""
if has_stdlib_scrypt or has_cffi_scrypt:
raise self.skipTest("non-builtin backend is present")
self.assertRaises(exc.MissingBackendError, scrypt_mod._set_backend, 'scrypt')
@skipUnless(has_cffi_scrypt, "'scrypt' package not found")
class ScryptPackageTest(_CommonScryptTest):
backend = "scrypt"
def test_default_backend(self):
"""backend management -- default backend"""
if has_stdlib_scrypt:
raise self.skipTest("higher priority backend present")
scrypt_mod._set_backend("default")
self.assertEqual(scrypt_mod.backend, "scrypt")
@skipUnless(has_stdlib_scrypt, "'hashlib.scrypt()' not found")
class StdlibScryptTest(_CommonScryptTest):
backend = "stdlib"
def test_default_backend(self):
"""backend management -- default backend"""
scrypt_mod._set_backend("default")
self.assertEqual(scrypt_mod.backend, "stdlib")
#=============================================================================
# eof
#=============================================================================

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
"""
test passlib.ext.django against django source tests
"""
#=============================================================================
# imports
#=============================================================================
from __future__ import absolute_import, division, print_function
# core
import logging; log = logging.getLogger(__name__)
# site
# pkg
from passlib.utils.compat import suppress_cause
from passlib.ext.django.utils import DJANGO_VERSION, DjangoTranslator, _PasslibHasherWrapper
# tests
from passlib.tests.utils import TestCase, TEST_MODE
from .test_ext_django import (
has_min_django, stock_config, _ExtensionSupport,
)
if has_min_django:
from .test_ext_django import settings
# local
__all__ = [
"HashersTest",
]
#=============================================================================
# HashersTest --
# hack up the some of the real django tests to run w/ extension loaded,
# to ensure we mimic their behavior.
# however, the django tests were moved out of the package, and into a source-only location
# as of django 1.7. so we disable tests from that point on unless test-runner specifies
#=============================================================================
#: ref to django unittest root module (if found)
test_hashers_mod = None
#: message about why test module isn't present (if not found)
hashers_skip_msg = None
#----------------------------------------------------------------------
# try to load django's tests/auth_tests/test_hasher.py module,
# or note why we failed.
#----------------------------------------------------------------------
if TEST_MODE(max="quick"):
hashers_skip_msg = "requires >= 'default' test mode"
elif has_min_django:
import os
import sys
source_path = os.environ.get("PASSLIB_TESTS_DJANGO_SOURCE_PATH")
if source_path:
if not os.path.exists(source_path):
raise EnvironmentError("django source path not found: %r" % source_path)
if not all(os.path.exists(os.path.join(source_path, name))
for name in ["django", "tests"]):
raise EnvironmentError("invalid django source path: %r" % source_path)
log.info("using django tests from source path: %r", source_path)
tests_path = os.path.join(source_path, "tests")
sys.path.insert(0, tests_path)
try:
from auth_tests import test_hashers as test_hashers_mod
except ImportError as err:
raise suppress_cause(
EnvironmentError("error trying to import django tests "
"from source path (%r): %r" %
(source_path, err)))
finally:
sys.path.remove(tests_path)
else:
hashers_skip_msg = "requires PASSLIB_TESTS_DJANGO_SOURCE_PATH to be set"
if TEST_MODE("full"):
# print warning so user knows what's happening
sys.stderr.write("\nWARNING: $PASSLIB_TESTS_DJANGO_SOURCE_PATH is not set; "
"can't run Django's own unittests against passlib.ext.django\n")
elif DJANGO_VERSION:
hashers_skip_msg = "django version too old"
else:
hashers_skip_msg = "django not installed"
#----------------------------------------------------------------------
# if found module, create wrapper to run django's own tests,
# but with passlib monkeypatched in.
#----------------------------------------------------------------------
if test_hashers_mod:
from django.core.signals import setting_changed
from django.dispatch import receiver
from django.utils.module_loading import import_string
from passlib.utils.compat import get_unbound_method_function
class HashersTest(test_hashers_mod.TestUtilsHashPass, _ExtensionSupport):
"""
Run django's hasher unittests against passlib's extension
and workalike implementations
"""
#==================================================================
# helpers
#==================================================================
# port patchAttr() helper method from passlib.tests.utils.TestCase
patchAttr = get_unbound_method_function(TestCase.patchAttr)
#==================================================================
# custom setup
#==================================================================
def setUp(self):
#---------------------------------------------------------
# install passlib.ext.django adapter, and get context
#---------------------------------------------------------
self.load_extension(PASSLIB_CONTEXT=stock_config, check=False)
from passlib.ext.django.models import adapter
context = adapter.context
#---------------------------------------------------------
# patch tests module to use our versions of patched funcs
# (which should be installed in hashers module)
#---------------------------------------------------------
from django.contrib.auth import hashers
for attr in ["make_password",
"check_password",
"identify_hasher",
"is_password_usable",
"get_hasher"]:
self.patchAttr(test_hashers_mod, attr, getattr(hashers, attr))
#---------------------------------------------------------
# django tests expect empty django_des_crypt salt field
#---------------------------------------------------------
from passlib.hash import django_des_crypt
self.patchAttr(django_des_crypt, "use_duplicate_salt", False)
#---------------------------------------------------------
# install receiver to update scheme list if test changes settings
#---------------------------------------------------------
django_to_passlib_name = DjangoTranslator().django_to_passlib_name
@receiver(setting_changed, weak=False)
def update_schemes(**kwds):
if kwds and kwds['setting'] != 'PASSWORD_HASHERS':
return
assert context is adapter.context
schemes = [
django_to_passlib_name(import_string(hash_path)())
for hash_path in settings.PASSWORD_HASHERS
]
# workaround for a few tests that only specify hex_md5,
# but test for django_salted_md5 format.
if "hex_md5" in schemes and "django_salted_md5" not in schemes:
schemes.append("django_salted_md5")
schemes.append("django_disabled")
context.update(schemes=schemes, deprecated="auto")
adapter.reset_hashers()
self.addCleanup(setting_changed.disconnect, update_schemes)
update_schemes()
#---------------------------------------------------------
# need password_context to keep up to date with django_hasher.iterations,
# which is frequently patched by django tests.
#
# HACK: to fix this, inserting wrapper around a bunch of context
# methods so that any time adapter calls them,
# attrs are resynced first.
#---------------------------------------------------------
def update_rounds():
"""
sync django hasher config -> passlib hashers
"""
for handler in context.schemes(resolve=True):
if 'rounds' not in handler.setting_kwds:
continue
hasher = adapter.passlib_to_django(handler)
if isinstance(hasher, _PasslibHasherWrapper):
continue
rounds = getattr(hasher, "rounds", None) or \
getattr(hasher, "iterations", None)
if rounds is None:
continue
# XXX: this doesn't modify the context, which would
# cause other weirdness (since it would replace handler factories completely,
# instead of just updating their state)
handler.min_desired_rounds = handler.max_desired_rounds = handler.default_rounds = rounds
_in_update = [False]
def update_wrapper(wrapped, *args, **kwds):
"""
wrapper around arbitrary func, that first triggers sync
"""
if not _in_update[0]:
_in_update[0] = True
try:
update_rounds()
finally:
_in_update[0] = False
return wrapped(*args, **kwds)
# sync before any context call
for attr in ["schemes", "handler", "default_scheme", "hash",
"verify", "needs_update", "verify_and_update"]:
self.patchAttr(context, attr, update_wrapper, wrap=True)
# sync whenever adapter tries to resolve passlib hasher
self.patchAttr(adapter, "django_to_passlib", update_wrapper, wrap=True)
def tearDown(self):
# NOTE: could rely on addCleanup() instead, but need py26 compat
self.unload_extension()
super(HashersTest, self).tearDown()
#==================================================================
# skip a few methods that can't be replicated properly
# *want to minimize these as much as possible*
#==================================================================
_OMIT = lambda self: self.skipTest("omitted by passlib")
# XXX: this test registers two classes w/ same algorithm id,
# something we don't support -- how does django sanely handle
# that anyways? get_hashers_by_algorithm() should throw KeyError, right?
test_pbkdf2_upgrade_new_hasher = _OMIT
# TODO: support wrapping django's harden-runtime feature?
# would help pass their tests.
test_check_password_calls_harden_runtime = _OMIT
test_bcrypt_harden_runtime = _OMIT
test_pbkdf2_harden_runtime = _OMIT
#==================================================================
# eoc
#==================================================================
else:
# otherwise leave a stub so test log tells why test was skipped.
class HashersTest(TestCase):
def test_external_django_hasher_tests(self):
"""external django hasher tests"""
raise self.skipTest(hashers_skip_msg)
#=============================================================================
# eof
#=============================================================================

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,507 @@
"""passlib.tests.test_handlers_argon2 - tests for passlib hash algorithms"""
#=============================================================================
# imports
#=============================================================================
# core
import logging
log = logging.getLogger(__name__)
import re
import warnings
# site
# pkg
from passlib import hash
from passlib.utils.compat import unicode
from passlib.tests.utils import HandlerCase, TEST_MODE
from passlib.tests.test_handlers import UPASS_TABLE, PASS_TABLE_UTF8
# module
#=============================================================================
# a bunch of tests lifted nearlky verbatim from official argon2 UTs...
# https://github.com/P-H-C/phc-winner-argon2/blob/master/src/test.c
#=============================================================================
def hashtest(version, t, logM, p, secret, salt, hex_digest, hash):
return dict(version=version, rounds=t, logM=logM, memory_cost=1<<logM, parallelism=p,
secret=secret, salt=salt, hex_digest=hex_digest, hash=hash)
# version 1.3 "I" tests
version = 0x10
reference_data = [
hashtest(version, 2, 16, 1, "password", "somesalt",
"f6c4db4a54e2a370627aff3db6176b94a2a209a62c8e36152711802f7b30c694",
"$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ"
"$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ"),
hashtest(version, 2, 20, 1, "password", "somesalt",
"9690ec55d28d3ed32562f2e73ea62b02b018757643a2ae6e79528459de8106e9",
"$argon2i$m=1048576,t=2,p=1$c29tZXNhbHQ"
"$lpDsVdKNPtMlYvLnPqYrArAYdXZDoq5ueVKEWd6BBuk"),
hashtest(version, 2, 18, 1, "password", "somesalt",
"3e689aaa3d28a77cf2bc72a51ac53166761751182f1ee292e3f677a7da4c2467",
"$argon2i$m=262144,t=2,p=1$c29tZXNhbHQ"
"$Pmiaqj0op3zyvHKlGsUxZnYXURgvHuKS4/Z3p9pMJGc"),
hashtest(version, 2, 8, 1, "password", "somesalt",
"fd4dd83d762c49bdeaf57c47bdcd0c2f1babf863fdeb490df63ede9975fccf06",
"$argon2i$m=256,t=2,p=1$c29tZXNhbHQ"
"$/U3YPXYsSb3q9XxHvc0MLxur+GP960kN9j7emXX8zwY"),
hashtest(version, 2, 8, 2, "password", "somesalt",
"b6c11560a6a9d61eac706b79a2f97d68b4463aa3ad87e00c07e2b01e90c564fb",
"$argon2i$m=256,t=2,p=2$c29tZXNhbHQ"
"$tsEVYKap1h6scGt5ovl9aLRGOqOth+AMB+KwHpDFZPs"),
hashtest(version, 1, 16, 1, "password", "somesalt",
"81630552b8f3b1f48cdb1992c4c678643d490b2b5eb4ff6c4b3438b5621724b2",
"$argon2i$m=65536,t=1,p=1$c29tZXNhbHQ"
"$gWMFUrjzsfSM2xmSxMZ4ZD1JCytetP9sSzQ4tWIXJLI"),
hashtest(version, 4, 16, 1, "password", "somesalt",
"f212f01615e6eb5d74734dc3ef40ade2d51d052468d8c69440a3a1f2c1c2847b",
"$argon2i$m=65536,t=4,p=1$c29tZXNhbHQ"
"$8hLwFhXm6110c03D70Ct4tUdBSRo2MaUQKOh8sHChHs"),
hashtest(version, 2, 16, 1, "differentpassword", "somesalt",
"e9c902074b6754531a3a0be519e5baf404b30ce69b3f01ac3bf21229960109a3",
"$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ"
"$6ckCB0tnVFMaOgvlGeW69ASzDOabPwGsO/ISKZYBCaM"),
hashtest(version, 2, 16, 1, "password", "diffsalt",
"79a103b90fe8aef8570cb31fc8b22259778916f8336b7bdac3892569d4f1c497",
"$argon2i$m=65536,t=2,p=1$ZGlmZnNhbHQ"
"$eaEDuQ/orvhXDLMfyLIiWXeJFvgza3vaw4kladTxxJc"),
]
# version 1.9 "I" tests
version = 0x13
reference_data.extend([
hashtest(version, 2, 16, 1, "password", "somesalt",
"c1628832147d9720c5bd1cfd61367078729f6dfb6f8fea9ff98158e0d7816ed0",
"$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ"
"$wWKIMhR9lyDFvRz9YTZweHKfbftvj+qf+YFY4NeBbtA"),
hashtest(version, 2, 20, 1, "password", "somesalt",
"d1587aca0922c3b5d6a83edab31bee3c4ebaef342ed6127a55d19b2351ad1f41",
"$argon2i$v=19$m=1048576,t=2,p=1$c29tZXNhbHQ"
"$0Vh6ygkiw7XWqD7asxvuPE667zQu1hJ6VdGbI1GtH0E"),
hashtest(version, 2, 18, 1, "password", "somesalt",
"296dbae80b807cdceaad44ae741b506f14db0959267b183b118f9b24229bc7cb",
"$argon2i$v=19$m=262144,t=2,p=1$c29tZXNhbHQ"
"$KW266AuAfNzqrUSudBtQbxTbCVkmexg7EY+bJCKbx8s"),
hashtest(version, 2, 8, 1, "password", "somesalt",
"89e9029f4637b295beb027056a7336c414fadd43f6b208645281cb214a56452f",
"$argon2i$v=19$m=256,t=2,p=1$c29tZXNhbHQ"
"$iekCn0Y3spW+sCcFanM2xBT63UP2sghkUoHLIUpWRS8"),
hashtest(version, 2, 8, 2, "password", "somesalt",
"4ff5ce2769a1d7f4c8a491df09d41a9fbe90e5eb02155a13e4c01e20cd4eab61",
"$argon2i$v=19$m=256,t=2,p=2$c29tZXNhbHQ"
"$T/XOJ2mh1/TIpJHfCdQan76Q5esCFVoT5MAeIM1Oq2E"),
hashtest(version, 1, 16, 1, "password", "somesalt",
"d168075c4d985e13ebeae560cf8b94c3b5d8a16c51916b6f4ac2da3ac11bbecf",
"$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQ"
"$0WgHXE2YXhPr6uVgz4uUw7XYoWxRkWtvSsLaOsEbvs8"),
hashtest(version, 4, 16, 1, "password", "somesalt",
"aaa953d58af3706ce3df1aefd4a64a84e31d7f54175231f1285259f88174ce5b",
"$argon2i$v=19$m=65536,t=4,p=1$c29tZXNhbHQ"
"$qqlT1YrzcGzj3xrv1KZKhOMdf1QXUjHxKFJZ+IF0zls"),
hashtest(version, 2, 16, 1, "differentpassword", "somesalt",
"14ae8da01afea8700c2358dcef7c5358d9021282bd88663a4562f59fb74d22ee",
"$argon2i$v=19$m=65536,t=2,p=1$c29tZXNhbHQ"
"$FK6NoBr+qHAMI1jc73xTWNkCEoK9iGY6RWL1n7dNIu4"),
hashtest(version, 2, 16, 1, "password", "diffsalt",
"b0357cccfbef91f3860b0dba447b2348cbefecadaf990abfe9cc40726c521271",
"$argon2i$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQ"
"$sDV8zPvvkfOGCw26RHsjSMvv7K2vmQq/6cxAcmxSEnE"),
])
# version 1.9 "ID" tests
version = 0x13
reference_data.extend([
hashtest(version, 2, 16, 1, "password", "somesalt",
"09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7",
"$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ"
"$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc"),
hashtest(version, 2, 18, 1, "password", "somesalt",
"78fe1ec91fb3aa5657d72e710854e4c3d9b9198c742f9616c2f085bed95b2e8c",
"$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ"
"$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLow"),
hashtest(version, 2, 8, 1, "password", "somesalt",
"9dfeb910e80bad0311fee20f9c0e2b12c17987b4cac90c2ef54d5b3021c68bfe",
"$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ"
"$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"),
hashtest(version, 2, 8, 2, "password", "somesalt",
"6d093c501fd5999645e0ea3bf620d7b8be7fd2db59c20d9fff9539da2bf57037",
"$argon2id$v=19$m=256,t=2,p=2$c29tZXNhbHQ"
"$bQk8UB/VmZZF4Oo79iDXuL5/0ttZwg2f/5U52iv1cDc"),
hashtest(version, 1, 16, 1, "password", "somesalt",
"f6a5adc1ba723dddef9b5ac1d464e180fcd9dffc9d1cbf76cca2fed795d9ca98",
"$argon2id$v=19$m=65536,t=1,p=1$c29tZXNhbHQ"
"$9qWtwbpyPd3vm1rB1GThgPzZ3/ydHL92zKL+15XZypg"),
hashtest(version, 4, 16, 1, "password", "somesalt",
"9025d48e68ef7395cca9079da4c4ec3affb3c8911fe4f86d1a2520856f63172c",
"$argon2id$v=19$m=65536,t=4,p=1$c29tZXNhbHQ"
"$kCXUjmjvc5XMqQedpMTsOv+zyJEf5PhtGiUghW9jFyw"),
hashtest(version, 2, 16, 1, "differentpassword", "somesalt",
"0b84d652cf6b0c4beaef0dfe278ba6a80df6696281d7e0d2891b817d8c458fde",
"$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ"
"$C4TWUs9rDEvq7w3+J4umqA32aWKB1+DSiRuBfYxFj94"),
hashtest(version, 2, 16, 1, "password", "diffsalt",
"bdf32b05ccc42eb15d58fd19b1f856b113da1e9a5874fdcc544308565aa8141c",
"$argon2id$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQ"
"$vfMrBczELrFdWP0ZsfhWsRPaHppYdP3MVEMIVlqoFBw"),
])
#=============================================================================
# argon2
#=============================================================================
class _base_argon2_test(HandlerCase):
handler = hash.argon2
known_correct_hashes = [
#
# custom
#
# sample test
("password", '$argon2i$v=19$m=256,t=1,p=1$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A'),
# sample w/ all parameters different
("password", '$argon2i$v=19$m=380,t=2,p=2$c29tZXNhbHQ$SrssP8n7m/12VWPM8dvNrw'),
# ensures utf-8 used for unicode
(UPASS_TABLE, '$argon2i$v=19$m=512,t=2,p=2$1sV0O4PWLtc12Ypv1f7oGw$'
'z+yqzlKtrq3SaNfXDfIDnQ'),
(PASS_TABLE_UTF8, '$argon2i$v=19$m=512,t=2,p=2$1sV0O4PWLtc12Ypv1f7oGw$'
'z+yqzlKtrq3SaNfXDfIDnQ'),
# ensure trailing null bytes handled correctly
('password\x00', '$argon2i$v=19$m=512,t=2,p=2$c29tZXNhbHQ$Fb5+nPuLzZvtqKRwqUEtUQ'),
# sample with type D (generated via argon_cffi2.PasswordHasher)
("password", '$argon2d$v=19$m=102400,t=2,p=8$g2RodLh8j8WbSdCp+lUy/A$zzAJqL/HSjm809PYQu6qkA'),
]
known_malformed_hashes = [
# unknown hash type
"$argon2qq$v=19$t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY",
# missing 'm' param
"$argon2i$v=19$t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY",
# 't' param > max uint32
"$argon2i$v=19$m=65536,t=8589934592,p=4$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY",
# unexpected param
"$argon2i$v=19$m=65536,t=2,p=4,q=5$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY",
# wrong param order
"$argon2i$v=19$t=2,m=65536,p=4,q=5$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY",
# constraint violation: m < 8 * p
"$argon2i$v=19$m=127,t=2,p=16$c29tZXNhbHQ$IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4",
]
known_parsehash_results = [
('$argon2i$v=19$m=256,t=2,p=3$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A',
dict(type="i", memory_cost=256, rounds=2, parallelism=3, salt=b'somesalt',
checksum=b'\x00\x91H\xb0\xd6S0\xa4\xc0{\x00x\xf8D\xcd\xd4')),
]
def setUpWarnings(self):
super(_base_argon2_test, self).setUpWarnings()
warnings.filterwarnings("ignore", ".*Using argon2pure backend.*")
def do_stub_encrypt(self, handler=None, **settings):
if self.backend == "argon2_cffi":
# overriding default since no way to get stub config from argon2._calc_hash()
# (otherwise test_21b_max_rounds blocks trying to do max rounds)
handler = (handler or self.handler).using(**settings)
self = handler(use_defaults=True)
self.checksum = self._stub_checksum
assert self.checksum
return self.to_string()
else:
return super(_base_argon2_test, self).do_stub_encrypt(handler, **settings)
def test_03_legacy_hash_workflow(self):
# override base method
raise self.skipTest("legacy 1.6 workflow not supported")
def test_keyid_parameter(self):
# NOTE: keyid parameter currently not supported by official argon2 hash parser,
# even though it's mentioned in the format spec.
# we're trying to be consistent w/ this, so hashes w/ keyid should
# always through a NotImplementedError.
self.assertRaises(NotImplementedError, self.handler.verify, 'password',
"$argon2i$v=19$m=65536,t=2,p=4,keyid=ABCD$c29tZXNhbHQ$"
"IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4")
def test_data_parameter(self):
# NOTE: argon2 c library doesn't support passing in a data parameter to argon2_hash();
# but argon2_verify() appears to parse that info... but then discards it (!?).
# not sure what proper behavior is, filed issue -- https://github.com/P-H-C/phc-winner-argon2/issues/143
# For now, replicating behavior we have for the two backends, to detect when things change.
handler = self.handler
# ref hash of 'password' when 'data' is correctly passed into argon2()
sample1 = '$argon2i$v=19$m=512,t=2,p=2,data=c29tZWRhdGE$c29tZXNhbHQ$KgHyCesFyyjkVkihZ5VNFw'
# ref hash of 'password' when 'data' is silently discarded (same digest as w/o data)
sample2 = '$argon2i$v=19$m=512,t=2,p=2,data=c29tZWRhdGE$c29tZXNhbHQ$uEeXt1dxN1iFKGhklseW4w'
# hash of 'password' w/o the data field
sample3 = '$argon2i$v=19$m=512,t=2,p=2$c29tZXNhbHQ$uEeXt1dxN1iFKGhklseW4w'
#
# test sample 1
#
if self.backend == "argon2_cffi":
# argon2_cffi v16.1 would incorrectly return False here.
# but v16.2 patches so it throws error on data parameter.
# our code should detect that, and adapt it into a NotImplementedError
self.assertRaises(NotImplementedError, handler.verify, "password", sample1)
# incorrectly returns sample3, dropping data parameter
self.assertEqual(handler.genhash("password", sample1), sample3)
else:
assert self.backend == "argon2pure"
# should parse and verify
self.assertTrue(handler.verify("password", sample1))
# should preserve sample1
self.assertEqual(handler.genhash("password", sample1), sample1)
#
# test sample 2
#
if self.backend == "argon2_cffi":
# argon2_cffi v16.1 would incorrectly return True here.
# but v16.2 patches so it throws error on data parameter.
# our code should detect that, and adapt it into a NotImplementedError
self.assertRaises(NotImplementedError, handler.verify,"password", sample2)
# incorrectly returns sample3, dropping data parameter
self.assertEqual(handler.genhash("password", sample1), sample3)
else:
assert self.backend == "argon2pure"
# should parse, but fail to verify
self.assertFalse(self.handler.verify("password", sample2))
# should return sample1 (corrected digest)
self.assertEqual(handler.genhash("password", sample2), sample1)
def test_keyid_and_data_parameters(self):
# test combination of the two, just in case
self.assertRaises(NotImplementedError, self.handler.verify, 'stub',
"$argon2i$v=19$m=65536,t=2,p=4,keyid=ABCD,data=EFGH$c29tZXNhbHQ$"
"IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4")
def test_type_kwd(self):
cls = self.handler
# XXX: this mirrors test_30_HasManyIdents();
# maybe switch argon2 class to use that mixin instead of "type" kwd?
# check settings
self.assertTrue("type" in cls.setting_kwds)
# check supported type_values
for value in cls.type_values:
self.assertIsInstance(value, unicode)
self.assertTrue("i" in cls.type_values)
self.assertTrue("d" in cls.type_values)
# check default
self.assertTrue(cls.type in cls.type_values)
# check constructor validates ident correctly.
handler = cls
hash = self.get_sample_hash()[1]
kwds = handler.parsehash(hash)
del kwds['type']
# ... accepts good type
handler(type=cls.type, **kwds)
# XXX: this is policy "ident" uses, maybe switch to it?
# # ... requires type w/o defaults
# self.assertRaises(TypeError, handler, **kwds)
handler(**kwds)
# ... supplies default type
handler(use_defaults=True, **kwds)
# ... rejects bad type
self.assertRaises(ValueError, handler, type='xXx', **kwds)
def test_type_using(self):
handler = self.handler
# XXX: this mirrors test_has_many_idents_using();
# maybe switch argon2 class to use that mixin instead of "type" kwd?
orig_type = handler.type
for alt_type in handler.type_values:
if alt_type != orig_type:
break
else:
raise AssertionError("expected to find alternate type: default=%r values=%r" %
(orig_type, handler.type_values))
def effective_type(cls):
return cls(use_defaults=True).type
# keep default if nothing else specified
subcls = handler.using()
self.assertEqual(subcls.type, orig_type)
# accepts alt type
subcls = handler.using(type=alt_type)
self.assertEqual(subcls.type, alt_type)
self.assertEqual(handler.type, orig_type)
# check subcls actually *generates* default type,
# and that we didn't affect orig handler
self.assertEqual(effective_type(subcls), alt_type)
self.assertEqual(effective_type(handler), orig_type)
# rejects bad type
self.assertRaises(ValueError, handler.using, type='xXx')
# honor 'type' alias
subcls = handler.using(type=alt_type)
self.assertEqual(subcls.type, alt_type)
self.assertEqual(handler.type, orig_type)
# check type aliases are being honored
self.assertEqual(effective_type(handler.using(type="I")), "i")
def test_needs_update_w_type(self):
handler = self.handler
hash = handler.hash("stub")
self.assertFalse(handler.needs_update(hash))
hash2 = re.sub(r"\$argon2\w+\$", "$argon2d$", hash)
self.assertTrue(handler.needs_update(hash2))
def test_needs_update_w_version(self):
handler = self.handler.using(memory_cost=65536, time_cost=2, parallelism=4,
digest_size=32)
hash = ("$argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$"
"QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY")
if handler.max_version == 0x10:
self.assertFalse(handler.needs_update(hash))
else:
self.assertTrue(handler.needs_update(hash))
def test_argon_byte_encoding(self):
"""verify we're using right base64 encoding for argon2"""
handler = self.handler
if handler.version != 0x13:
# TODO: make this fatal, and add refs for other version.
raise self.skipTest("handler uses wrong version for sample hashes")
# 8 byte salt
salt = b'somesalt'
temp = handler.using(memory_cost=256, time_cost=2, parallelism=2, salt=salt,
checksum_size=32, type="i")
hash = temp.hash("password")
self.assertEqual(hash, "$argon2i$v=19$m=256,t=2,p=2"
"$c29tZXNhbHQ"
"$T/XOJ2mh1/TIpJHfCdQan76Q5esCFVoT5MAeIM1Oq2E")
# 16 byte salt
salt = b'somesalt\x00\x00\x00\x00\x00\x00\x00\x00'
temp = handler.using(memory_cost=256, time_cost=2, parallelism=2, salt=salt,
checksum_size=32, type="i")
hash = temp.hash("password")
self.assertEqual(hash, "$argon2i$v=19$m=256,t=2,p=2"
"$c29tZXNhbHQAAAAAAAAAAA"
"$rqnbEp1/jFDUEKZZmw+z14amDsFqMDC53dIe57ZHD38")
class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
settings_map = HandlerCase.FuzzHashGenerator.settings_map.copy()
settings_map.update(memory_cost="random_memory_cost", type="random_type")
def random_type(self):
return self.rng.choice(self.handler.type_values)
def random_memory_cost(self):
if self.test.backend == "argon2pure":
return self.randintgauss(128, 384, 256, 128)
else:
return self.randintgauss(128, 32767, 16384, 4096)
# TODO: fuzz parallelism, digest_size
#-----------------------------------------
# test suites for specific backends
#-----------------------------------------
class argon2_argon2_cffi_test(_base_argon2_test.create_backend_case("argon2_cffi")):
# add some more test vectors that take too long under argon2pure
known_correct_hashes = _base_argon2_test.known_correct_hashes + [
#
# sample hashes from argon2 cffi package's unittests,
# which in turn were generated by official argon2 cmdline tool.
#
# v1.2, type I, w/o a version tag
('password', "$argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$"
"QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY"),
# v1.3, type I
('password', "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$"
"IMit9qkFULCMA/ViizL57cnTLOa5DiVM9eMwpAvPwr4"),
# v1.3, type D
('password', "$argon2d$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$"
"cZn5d+rFh+ZfuRhm2iGUGgcrW5YLeM6q7L3vBsdmFA0"),
# v1.3, type ID
('password', "$argon2id$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$"
"GpZ3sK/oH9p7VIiV56G/64Zo/8GaUw434IimaPqxwCo"),
#
# custom
#
# ensure trailing null bytes handled correctly
('password\x00', "$argon2i$v=19$m=65536,t=2,p=4$c29tZXNhbHQ$"
"Vpzuc0v0SrP88LcVvmg+z5RoOYpMDKH/lt6O+CZabIQ"),
]
# add reference hashes from argon2 clib tests
known_correct_hashes.extend(
(info['secret'], info['hash']) for info in reference_data
if info['logM'] <= (18 if TEST_MODE("full") else 16)
)
class argon2_argon2pure_test(_base_argon2_test.create_backend_case("argon2pure")):
# XXX: setting max_threads at 1 to prevent argon2pure from using multiprocessing,
# which causes big problems when testing under pypy.
# would like a "pure_use_threads" option instead, to make it use multiprocessing.dummy instead.
handler = hash.argon2.using(memory_cost=32, parallelism=2)
# don't use multiprocessing for unittests, makes it a lot harder to ctrl-c
# XXX: make this controlled by env var?
handler.pure_use_threads = True
# add reference hashes from argon2 clib tests
known_correct_hashes = _base_argon2_test.known_correct_hashes[:]
known_correct_hashes.extend(
(info['secret'], info['hash']) for info in reference_data
if info['logM'] < 16
)
class FuzzHashGenerator(_base_argon2_test.FuzzHashGenerator):
def random_rounds(self):
# decrease default rounds for fuzz testing to speed up volume.
return self.randintgauss(1, 3, 2, 1)
#=============================================================================
# eof
#=============================================================================