first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""passlib tests"""
|
||||
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
from nose import run
|
||||
run(
|
||||
defaultTest=os.path.dirname(__file__),
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
"""helper for method in test_registry.py"""
|
||||
from passlib.registry import register_crypt_handler
|
||||
import passlib.utils.handlers as uh
|
||||
|
||||
class dummy_bad(uh.StaticHandler):
|
||||
name = "dummy_bad"
|
||||
|
||||
class alt_dummy_bad(uh.StaticHandler):
|
||||
name = "dummy_bad"
|
||||
|
||||
# NOTE: if passlib.tests is being run from symlink (e.g. via gaeunit),
|
||||
# this module may be imported a second time as test._test_bad_registry.
|
||||
# we don't want it to do anything in that case.
|
||||
if __name__.startswith("passlib.tests"):
|
||||
register_crypt_handler(alt_dummy_bad)
|
||||
67
venv/lib/python3.12/site-packages/passlib/tests/backports.py
Normal file
67
venv/lib/python3.12/site-packages/passlib/tests/backports.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""backports of needed unittest2 features"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
import re
|
||||
import sys
|
||||
##from warnings import warn
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils.compat import PY26
|
||||
# local
|
||||
__all__ = [
|
||||
"TestCase",
|
||||
"unittest",
|
||||
# TODO: deprecate these exports in favor of "unittest.XXX"
|
||||
"skip", "skipIf", "skipUnless",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# import latest unittest module available
|
||||
#=============================================================================
|
||||
try:
|
||||
import unittest2 as unittest
|
||||
except ImportError:
|
||||
if PY26:
|
||||
raise ImportError("Passlib's tests require 'unittest2' under Python 2.6 (as of Passlib 1.7)")
|
||||
# python 2.7 and python 3.2 both have unittest2 features (at least, the ones we use)
|
||||
import unittest
|
||||
|
||||
#=============================================================================
|
||||
# unittest aliases
|
||||
#=============================================================================
|
||||
skip = unittest.skip
|
||||
skipIf = unittest.skipIf
|
||||
skipUnless = unittest.skipUnless
|
||||
SkipTest = unittest.SkipTest
|
||||
|
||||
#=============================================================================
|
||||
# custom test harness
|
||||
#=============================================================================
|
||||
class TestCase(unittest.TestCase):
|
||||
"""backports a number of unittest2 features in TestCase"""
|
||||
|
||||
#===================================================================
|
||||
# backport some unittest2 names
|
||||
#===================================================================
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# backport assertRegex() alias from 3.2 to 2.7
|
||||
# was present in 2.7 under an alternate name
|
||||
#---------------------------------------------------------------
|
||||
if not hasattr(unittest.TestCase, "assertRegex"):
|
||||
assertRegex = unittest.TestCase.assertRegexpMatches
|
||||
|
||||
if not hasattr(unittest.TestCase, "assertRaisesRegex"):
|
||||
assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,9 @@
|
||||
[passlib]
|
||||
schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
|
||||
default = md5_crypt
|
||||
all__vary_rounds = 0.1
|
||||
bsdi_crypt__default_rounds = 25001
|
||||
bsdi_crypt__max_rounds = 30001
|
||||
sha512_crypt__max_rounds = 50000
|
||||
sha512_crypt__min_rounds = 40000
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[passlib]
|
||||
schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
|
||||
default = md5_crypt
|
||||
all__vary_rounds = 0.1
|
||||
bsdi_crypt__default_rounds = 25001
|
||||
bsdi_crypt__max_rounds = 30001
|
||||
sha512_crypt__max_rounds = 50000
|
||||
sha512_crypt__min_rounds = 40000
|
||||
|
||||
BIN
venv/lib/python3.12/site-packages/passlib/tests/sample1c.cfg
Normal file
BIN
venv/lib/python3.12/site-packages/passlib/tests/sample1c.cfg
Normal file
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
[passlib]
|
||||
schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
|
||||
default = md5_crypt
|
||||
all.vary_rounds = 10%%
|
||||
bsdi_crypt.max_rounds = 30000
|
||||
bsdi_crypt.default_rounds = 25000
|
||||
sha512_crypt.max_rounds = 50000
|
||||
sha512_crypt.min_rounds = 40000
|
||||
769
venv/lib/python3.12/site-packages/passlib/tests/test_apache.py
Normal file
769
venv/lib/python3.12/site-packages/passlib/tests/test_apache.py
Normal file
@@ -0,0 +1,769 @@
|
||||
"""tests for passlib.apache -- (c) Assurance Technologies 2008-2011"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
from logging import getLogger
|
||||
import os
|
||||
import subprocess
|
||||
# site
|
||||
# pkg
|
||||
from passlib import apache, registry
|
||||
from passlib.exc import MissingBackendError
|
||||
from passlib.utils.compat import irange
|
||||
from passlib.tests.backports import unittest
|
||||
from passlib.tests.utils import TestCase, get_file, set_file, ensure_mtime_changed
|
||||
from passlib.utils.compat import u
|
||||
from passlib.utils import to_bytes
|
||||
from passlib.utils.handlers import to_unicode_for_identify
|
||||
# module
|
||||
log = getLogger(__name__)
|
||||
|
||||
#=============================================================================
|
||||
# helpers
|
||||
#=============================================================================
|
||||
|
||||
def backdate_file_mtime(path, offset=10):
|
||||
"""backdate file's mtime by specified amount"""
|
||||
# NOTE: this is used so we can test code which detects mtime changes,
|
||||
# without having to actually *pause* for that long.
|
||||
atime = os.path.getatime(path)
|
||||
mtime = os.path.getmtime(path)-offset
|
||||
os.utime(path, (atime, mtime))
|
||||
|
||||
#=============================================================================
|
||||
# detect external HTPASSWD tool
|
||||
#=============================================================================
|
||||
|
||||
|
||||
htpasswd_path = os.environ.get("PASSLIB_TEST_HTPASSWD_PATH") or "htpasswd"
|
||||
|
||||
|
||||
def _call_htpasswd(args, stdin=None):
|
||||
"""
|
||||
helper to run htpasswd cmd
|
||||
"""
|
||||
if stdin is not None:
|
||||
stdin = stdin.encode("utf-8")
|
||||
proc = subprocess.Popen([htpasswd_path] + args, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, stdin=subprocess.PIPE if stdin else None)
|
||||
out, err = proc.communicate(stdin)
|
||||
rc = proc.wait()
|
||||
out = to_unicode_for_identify(out or "")
|
||||
return out, rc
|
||||
|
||||
|
||||
def _call_htpasswd_verify(path, user, password):
|
||||
"""
|
||||
wrapper for htpasswd verify
|
||||
"""
|
||||
out, rc = _call_htpasswd(["-vi", path, user], password)
|
||||
return not rc
|
||||
|
||||
|
||||
def _detect_htpasswd():
|
||||
"""
|
||||
helper to check if htpasswd is present
|
||||
"""
|
||||
try:
|
||||
out, rc = _call_htpasswd([])
|
||||
except OSError:
|
||||
# TODO: under py3, could trap the more specific FileNotFoundError
|
||||
# cmd not found
|
||||
return False, False
|
||||
# when called w/o args, it should print usage to stderr & return rc=2
|
||||
if not rc:
|
||||
log.warning("htpasswd test returned with rc=0")
|
||||
have_bcrypt = " -B " in out
|
||||
return True, have_bcrypt
|
||||
|
||||
|
||||
HAVE_HTPASSWD, HAVE_HTPASSWD_BCRYPT = _detect_htpasswd()
|
||||
|
||||
requires_htpasswd_cmd = unittest.skipUnless(HAVE_HTPASSWD, "requires `htpasswd` cmdline tool")
|
||||
|
||||
|
||||
#=============================================================================
|
||||
# htpasswd
|
||||
#=============================================================================
|
||||
class HtpasswdFileTest(TestCase):
|
||||
"""test HtpasswdFile class"""
|
||||
descriptionPrefix = "HtpasswdFile"
|
||||
|
||||
# sample with 4 users
|
||||
sample_01 = (b'user2:2CHkkwa2AtqGs\n'
|
||||
b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
|
||||
b'user4:pass4\n'
|
||||
b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n')
|
||||
|
||||
# sample 1 with user 1, 2 deleted; 4 changed
|
||||
sample_02 = b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\nuser4:pass4\n'
|
||||
|
||||
# sample 1 with user2 updated, user 1 first entry removed, and user 5 added
|
||||
sample_03 = (b'user2:pass2x\n'
|
||||
b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
|
||||
b'user4:pass4\n'
|
||||
b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n'
|
||||
b'user5:pass5\n')
|
||||
|
||||
# standalone sample with 8-bit username
|
||||
sample_04_utf8 = b'user\xc3\xa6:2CHkkwa2AtqGs\n'
|
||||
sample_04_latin1 = b'user\xe6:2CHkkwa2AtqGs\n'
|
||||
|
||||
sample_dup = b'user1:pass1\nuser1:pass2\n'
|
||||
|
||||
# sample with bcrypt & sha256_crypt hashes
|
||||
sample_05 = (b'user2:2CHkkwa2AtqGs\n'
|
||||
b'user3:{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=\n'
|
||||
b'user4:pass4\n'
|
||||
b'user1:$apr1$t4tc7jTh$GPIWVUo8sQKJlUdV8V5vu0\n'
|
||||
b'user5:$2a$12$yktDxraxijBZ360orOyCOePFGhuis/umyPNJoL5EbsLk.s6SWdrRO\n'
|
||||
b'user6:$5$rounds=110000$cCRp/xUUGVgwR4aP$'
|
||||
b'p0.QKFS5qLNRqw1/47lXYiAcgIjJK.WjCO8nrEKuUK.\n')
|
||||
|
||||
def test_00_constructor_autoload(self):
|
||||
"""test constructor autoload"""
|
||||
# check with existing file
|
||||
path = self.mktemp()
|
||||
set_file(path, self.sample_01)
|
||||
ht = apache.HtpasswdFile(path)
|
||||
self.assertEqual(ht.to_string(), self.sample_01)
|
||||
self.assertEqual(ht.path, path)
|
||||
self.assertTrue(ht.mtime)
|
||||
|
||||
# check changing path
|
||||
ht.path = path + "x"
|
||||
self.assertEqual(ht.path, path + "x")
|
||||
self.assertFalse(ht.mtime)
|
||||
|
||||
# check new=True
|
||||
ht = apache.HtpasswdFile(path, new=True)
|
||||
self.assertEqual(ht.to_string(), b"")
|
||||
self.assertEqual(ht.path, path)
|
||||
self.assertFalse(ht.mtime)
|
||||
|
||||
# check autoload=False (deprecated alias for new=True)
|
||||
with self.assertWarningList("``autoload=False`` is deprecated"):
|
||||
ht = apache.HtpasswdFile(path, autoload=False)
|
||||
self.assertEqual(ht.to_string(), b"")
|
||||
self.assertEqual(ht.path, path)
|
||||
self.assertFalse(ht.mtime)
|
||||
|
||||
# check missing file
|
||||
os.remove(path)
|
||||
self.assertRaises(IOError, apache.HtpasswdFile, path)
|
||||
|
||||
# NOTE: "default_scheme" option checked via set_password() test, among others
|
||||
|
||||
def test_00_from_path(self):
|
||||
path = self.mktemp()
|
||||
set_file(path, self.sample_01)
|
||||
ht = apache.HtpasswdFile.from_path(path)
|
||||
self.assertEqual(ht.to_string(), self.sample_01)
|
||||
self.assertEqual(ht.path, None)
|
||||
self.assertFalse(ht.mtime)
|
||||
|
||||
def test_01_delete(self):
|
||||
"""test delete()"""
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_01)
|
||||
self.assertTrue(ht.delete("user1")) # should delete both entries
|
||||
self.assertTrue(ht.delete("user2"))
|
||||
self.assertFalse(ht.delete("user5")) # user not present
|
||||
self.assertEqual(ht.to_string(), self.sample_02)
|
||||
|
||||
# invalid user
|
||||
self.assertRaises(ValueError, ht.delete, "user:")
|
||||
|
||||
def test_01_delete_autosave(self):
|
||||
path = self.mktemp()
|
||||
sample = b'user1:pass1\nuser2:pass2\n'
|
||||
set_file(path, sample)
|
||||
|
||||
ht = apache.HtpasswdFile(path)
|
||||
ht.delete("user1")
|
||||
self.assertEqual(get_file(path), sample)
|
||||
|
||||
ht = apache.HtpasswdFile(path, autosave=True)
|
||||
ht.delete("user1")
|
||||
self.assertEqual(get_file(path), b"user2:pass2\n")
|
||||
|
||||
def test_02_set_password(self):
|
||||
"""test set_password()"""
|
||||
ht = apache.HtpasswdFile.from_string(
|
||||
self.sample_01, default_scheme="plaintext")
|
||||
self.assertTrue(ht.set_password("user2", "pass2x"))
|
||||
self.assertFalse(ht.set_password("user5", "pass5"))
|
||||
self.assertEqual(ht.to_string(), self.sample_03)
|
||||
|
||||
# test legacy default kwd
|
||||
with self.assertWarningList("``default`` is deprecated"):
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_01, default="plaintext")
|
||||
self.assertTrue(ht.set_password("user2", "pass2x"))
|
||||
self.assertFalse(ht.set_password("user5", "pass5"))
|
||||
self.assertEqual(ht.to_string(), self.sample_03)
|
||||
|
||||
# invalid user
|
||||
self.assertRaises(ValueError, ht.set_password, "user:", "pass")
|
||||
|
||||
# test that legacy update() still works
|
||||
with self.assertWarningList("update\(\) is deprecated"):
|
||||
ht.update("user2", "test")
|
||||
self.assertTrue(ht.check_password("user2", "test"))
|
||||
|
||||
def test_02_set_password_autosave(self):
|
||||
path = self.mktemp()
|
||||
sample = b'user1:pass1\n'
|
||||
set_file(path, sample)
|
||||
|
||||
ht = apache.HtpasswdFile(path)
|
||||
ht.set_password("user1", "pass2")
|
||||
self.assertEqual(get_file(path), sample)
|
||||
|
||||
ht = apache.HtpasswdFile(path, default_scheme="plaintext", autosave=True)
|
||||
ht.set_password("user1", "pass2")
|
||||
self.assertEqual(get_file(path), b"user1:pass2\n")
|
||||
|
||||
def test_02_set_password_default_scheme(self):
|
||||
"""test set_password() -- default_scheme"""
|
||||
|
||||
def check(scheme):
|
||||
ht = apache.HtpasswdFile(default_scheme=scheme)
|
||||
ht.set_password("user1", "pass1")
|
||||
return ht.context.identify(ht.get_hash("user1"))
|
||||
|
||||
# explicit scheme
|
||||
self.assertEqual(check("sha256_crypt"), "sha256_crypt")
|
||||
self.assertEqual(check("des_crypt"), "des_crypt")
|
||||
|
||||
# unknown scheme
|
||||
self.assertRaises(KeyError, check, "xxx")
|
||||
|
||||
# alias resolution
|
||||
self.assertEqual(check("portable"), apache.htpasswd_defaults["portable"])
|
||||
self.assertEqual(check("portable_apache_22"), apache.htpasswd_defaults["portable_apache_22"])
|
||||
self.assertEqual(check("host_apache_22"), apache.htpasswd_defaults["host_apache_22"])
|
||||
|
||||
# default
|
||||
self.assertEqual(check(None), apache.htpasswd_defaults["portable_apache_22"])
|
||||
|
||||
def test_03_users(self):
|
||||
"""test users()"""
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_01)
|
||||
ht.set_password("user5", "pass5")
|
||||
ht.delete("user3")
|
||||
ht.set_password("user3", "pass3")
|
||||
self.assertEqual(sorted(ht.users()), ["user1", "user2", "user3", "user4", "user5"])
|
||||
|
||||
def test_04_check_password(self):
|
||||
"""test check_password()"""
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_05)
|
||||
self.assertRaises(TypeError, ht.check_password, 1, 'pass9')
|
||||
self.assertTrue(ht.check_password("user9","pass9") is None)
|
||||
|
||||
# users 1..6 of sample_01 run through all the main hash formats,
|
||||
# to make sure they're recognized.
|
||||
for i in irange(1, 7):
|
||||
i = str(i)
|
||||
try:
|
||||
self.assertTrue(ht.check_password("user"+i, "pass"+i))
|
||||
self.assertTrue(ht.check_password("user"+i, "pass9") is False)
|
||||
except MissingBackendError:
|
||||
if i == "5":
|
||||
# user5 uses bcrypt, which is apparently not available right now
|
||||
continue
|
||||
raise
|
||||
|
||||
self.assertRaises(ValueError, ht.check_password, "user:", "pass")
|
||||
|
||||
# test that legacy verify() still works
|
||||
with self.assertWarningList(["verify\(\) is deprecated"]*2):
|
||||
self.assertTrue(ht.verify("user1", "pass1"))
|
||||
self.assertFalse(ht.verify("user1", "pass2"))
|
||||
|
||||
def test_05_load(self):
|
||||
"""test load()"""
|
||||
# setup empty file
|
||||
path = self.mktemp()
|
||||
set_file(path, "")
|
||||
backdate_file_mtime(path, 5)
|
||||
ha = apache.HtpasswdFile(path, default_scheme="plaintext")
|
||||
self.assertEqual(ha.to_string(), b"")
|
||||
|
||||
# make changes, check load_if_changed() does nothing
|
||||
ha.set_password("user1", "pass1")
|
||||
ha.load_if_changed()
|
||||
self.assertEqual(ha.to_string(), b"user1:pass1\n")
|
||||
|
||||
# change file
|
||||
set_file(path, self.sample_01)
|
||||
ha.load_if_changed()
|
||||
self.assertEqual(ha.to_string(), self.sample_01)
|
||||
|
||||
# make changes, check load() overwrites them
|
||||
ha.set_password("user5", "pass5")
|
||||
ha.load()
|
||||
self.assertEqual(ha.to_string(), self.sample_01)
|
||||
|
||||
# test load w/ no path
|
||||
hb = apache.HtpasswdFile()
|
||||
self.assertRaises(RuntimeError, hb.load)
|
||||
self.assertRaises(RuntimeError, hb.load_if_changed)
|
||||
|
||||
# test load w/ dups and explicit path
|
||||
set_file(path, self.sample_dup)
|
||||
hc = apache.HtpasswdFile()
|
||||
hc.load(path)
|
||||
self.assertTrue(hc.check_password('user1','pass1'))
|
||||
|
||||
# NOTE: load_string() tested via from_string(), which is used all over this file
|
||||
|
||||
def test_06_save(self):
|
||||
"""test save()"""
|
||||
# load from file
|
||||
path = self.mktemp()
|
||||
set_file(path, self.sample_01)
|
||||
ht = apache.HtpasswdFile(path)
|
||||
|
||||
# make changes, check they saved
|
||||
ht.delete("user1")
|
||||
ht.delete("user2")
|
||||
ht.save()
|
||||
self.assertEqual(get_file(path), self.sample_02)
|
||||
|
||||
# test save w/ no path
|
||||
hb = apache.HtpasswdFile(default_scheme="plaintext")
|
||||
hb.set_password("user1", "pass1")
|
||||
self.assertRaises(RuntimeError, hb.save)
|
||||
|
||||
# test save w/ explicit path
|
||||
hb.save(path)
|
||||
self.assertEqual(get_file(path), b"user1:pass1\n")
|
||||
|
||||
def test_07_encodings(self):
|
||||
"""test 'encoding' kwd"""
|
||||
# test bad encodings cause failure in constructor
|
||||
self.assertRaises(ValueError, apache.HtpasswdFile, encoding="utf-16")
|
||||
|
||||
# check sample utf-8
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding="utf-8",
|
||||
return_unicode=True)
|
||||
self.assertEqual(ht.users(), [ u("user\u00e6") ])
|
||||
|
||||
# test deprecated encoding=None
|
||||
with self.assertWarningList("``encoding=None`` is deprecated"):
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_04_utf8, encoding=None)
|
||||
self.assertEqual(ht.users(), [ b'user\xc3\xa6' ])
|
||||
|
||||
# check sample latin-1
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_04_latin1,
|
||||
encoding="latin-1", return_unicode=True)
|
||||
self.assertEqual(ht.users(), [ u("user\u00e6") ])
|
||||
|
||||
def test_08_get_hash(self):
|
||||
"""test get_hash()"""
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_01)
|
||||
self.assertEqual(ht.get_hash("user3"), b"{SHA}3ipNV1GrBtxPmHFC21fCbVCSXIo=")
|
||||
self.assertEqual(ht.get_hash("user4"), b"pass4")
|
||||
self.assertEqual(ht.get_hash("user5"), None)
|
||||
|
||||
with self.assertWarningList("find\(\) is deprecated"):
|
||||
self.assertEqual(ht.find("user4"), b"pass4")
|
||||
|
||||
def test_09_to_string(self):
|
||||
"""test to_string"""
|
||||
|
||||
# check with known sample
|
||||
ht = apache.HtpasswdFile.from_string(self.sample_01)
|
||||
self.assertEqual(ht.to_string(), self.sample_01)
|
||||
|
||||
# test blank
|
||||
ht = apache.HtpasswdFile()
|
||||
self.assertEqual(ht.to_string(), b"")
|
||||
|
||||
def test_10_repr(self):
|
||||
ht = apache.HtpasswdFile("fakepath", autosave=True, new=True, encoding="latin-1")
|
||||
repr(ht)
|
||||
|
||||
def test_11_malformed(self):
|
||||
self.assertRaises(ValueError, apache.HtpasswdFile.from_string,
|
||||
b'realm:user1:pass1\n')
|
||||
self.assertRaises(ValueError, apache.HtpasswdFile.from_string,
|
||||
b'pass1\n')
|
||||
|
||||
def test_12_from_string(self):
|
||||
# forbid path kwd
|
||||
self.assertRaises(TypeError, apache.HtpasswdFile.from_string,
|
||||
b'', path=None)
|
||||
|
||||
def test_13_whitespace(self):
|
||||
"""whitespace & comment handling"""
|
||||
|
||||
# per htpasswd source (https://github.com/apache/httpd/blob/trunk/support/htpasswd.c),
|
||||
# lines that match "^\s*(#.*)?$" should be ignored
|
||||
source = to_bytes(
|
||||
'\n'
|
||||
'user2:pass2\n'
|
||||
'user4:pass4\n'
|
||||
'user7:pass7\r\n'
|
||||
' \t \n'
|
||||
'user1:pass1\n'
|
||||
' # legacy users\n'
|
||||
'#user6:pass6\n'
|
||||
'user5:pass5\n\n'
|
||||
)
|
||||
|
||||
# loading should see all users (except user6, who was commented out)
|
||||
ht = apache.HtpasswdFile.from_string(source)
|
||||
self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user7"])
|
||||
|
||||
# update existing user
|
||||
ht.set_hash("user4", "althash4")
|
||||
self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user7"])
|
||||
|
||||
# add a new user
|
||||
ht.set_hash("user6", "althash6")
|
||||
self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user6", "user7"])
|
||||
|
||||
# delete existing user
|
||||
ht.delete("user7")
|
||||
self.assertEqual(sorted(ht.users()), ["user1", "user2", "user4", "user5", "user6"])
|
||||
|
||||
# re-serialization should preserve whitespace
|
||||
target = to_bytes(
|
||||
'\n'
|
||||
'user2:pass2\n'
|
||||
'user4:althash4\n'
|
||||
' \t \n'
|
||||
'user1:pass1\n'
|
||||
' # legacy users\n'
|
||||
'#user6:pass6\n'
|
||||
'user5:pass5\n'
|
||||
'user6:althash6\n'
|
||||
)
|
||||
self.assertEqual(ht.to_string(), target)
|
||||
|
||||
@requires_htpasswd_cmd
|
||||
def test_htpasswd_cmd_verify(self):
|
||||
"""
|
||||
verify "htpasswd" command can read output
|
||||
"""
|
||||
path = self.mktemp()
|
||||
ht = apache.HtpasswdFile(path=path, new=True)
|
||||
|
||||
def hash_scheme(pwd, scheme):
|
||||
return ht.context.handler(scheme).hash(pwd)
|
||||
|
||||
# base scheme
|
||||
ht.set_hash("user1", hash_scheme("password","apr_md5_crypt"))
|
||||
|
||||
# 2.2-compat scheme
|
||||
host_no_bcrypt = apache.htpasswd_defaults["host_apache_22"]
|
||||
ht.set_hash("user2", hash_scheme("password", host_no_bcrypt))
|
||||
|
||||
# 2.4-compat scheme
|
||||
host_best = apache.htpasswd_defaults["host"]
|
||||
ht.set_hash("user3", hash_scheme("password", host_best))
|
||||
|
||||
# unsupported scheme -- should always fail to verify
|
||||
ht.set_hash("user4", "$xxx$foo$bar$baz")
|
||||
|
||||
# make sure htpasswd properly recognizes hashes
|
||||
ht.save()
|
||||
|
||||
self.assertFalse(_call_htpasswd_verify(path, "user1", "wrong"))
|
||||
self.assertFalse(_call_htpasswd_verify(path, "user2", "wrong"))
|
||||
self.assertFalse(_call_htpasswd_verify(path, "user3", "wrong"))
|
||||
self.assertFalse(_call_htpasswd_verify(path, "user4", "wrong"))
|
||||
|
||||
self.assertTrue(_call_htpasswd_verify(path, "user1", "password"))
|
||||
self.assertTrue(_call_htpasswd_verify(path, "user2", "password"))
|
||||
self.assertTrue(_call_htpasswd_verify(path, "user3", "password"))
|
||||
|
||||
@requires_htpasswd_cmd
|
||||
@unittest.skipUnless(registry.has_backend("bcrypt"), "bcrypt support required")
|
||||
def test_htpasswd_cmd_verify_bcrypt(self):
|
||||
"""
|
||||
verify "htpasswd" command can read bcrypt format
|
||||
|
||||
this tests for regression of issue 95, where we output "$2b$" instead of "$2y$";
|
||||
fixed in v1.7.2.
|
||||
"""
|
||||
path = self.mktemp()
|
||||
ht = apache.HtpasswdFile(path=path, new=True)
|
||||
def hash_scheme(pwd, scheme):
|
||||
return ht.context.handler(scheme).hash(pwd)
|
||||
ht.set_hash("user1", hash_scheme("password", "bcrypt"))
|
||||
ht.save()
|
||||
self.assertFalse(_call_htpasswd_verify(path, "user1", "wrong"))
|
||||
if HAVE_HTPASSWD_BCRYPT:
|
||||
self.assertTrue(_call_htpasswd_verify(path, "user1", "password"))
|
||||
else:
|
||||
# apache2.2 should fail, acting like it's an unknown hash format
|
||||
self.assertFalse(_call_htpasswd_verify(path, "user1", "password"))
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# htdigest
|
||||
#=============================================================================
|
||||
class HtdigestFileTest(TestCase):
|
||||
"""test HtdigestFile class"""
|
||||
descriptionPrefix = "HtdigestFile"
|
||||
|
||||
# sample with 4 users
|
||||
sample_01 = (b'user2:realm:549d2a5f4659ab39a80dac99e159ab19\n'
|
||||
b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
|
||||
b'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n'
|
||||
b'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n')
|
||||
|
||||
# sample 1 with user 1, 2 deleted; 4 changed
|
||||
sample_02 = (b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
|
||||
b'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n')
|
||||
|
||||
# sample 1 with user2 updated, user 1 first entry removed, and user 5 added
|
||||
sample_03 = (b'user2:realm:5ba6d8328943c23c64b50f8b29566059\n'
|
||||
b'user3:realm:a500bb8c02f6a9170ae46af10c898744\n'
|
||||
b'user4:realm:ab7b5d5f28ccc7666315f508c7358519\n'
|
||||
b'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n'
|
||||
b'user5:realm:03c55fdc6bf71552356ad401bdb9af19\n')
|
||||
|
||||
# standalone sample with 8-bit username & realm
|
||||
sample_04_utf8 = b'user\xc3\xa6:realm\xc3\xa6:549d2a5f4659ab39a80dac99e159ab19\n'
|
||||
sample_04_latin1 = b'user\xe6:realm\xe6:549d2a5f4659ab39a80dac99e159ab19\n'
|
||||
|
||||
def test_00_constructor_autoload(self):
|
||||
"""test constructor autoload"""
|
||||
# check with existing file
|
||||
path = self.mktemp()
|
||||
set_file(path, self.sample_01)
|
||||
ht = apache.HtdigestFile(path)
|
||||
self.assertEqual(ht.to_string(), self.sample_01)
|
||||
|
||||
# check without autoload
|
||||
ht = apache.HtdigestFile(path, new=True)
|
||||
self.assertEqual(ht.to_string(), b"")
|
||||
|
||||
# check missing file
|
||||
os.remove(path)
|
||||
self.assertRaises(IOError, apache.HtdigestFile, path)
|
||||
|
||||
# NOTE: default_realm option checked via other tests.
|
||||
|
||||
def test_01_delete(self):
|
||||
"""test delete()"""
|
||||
ht = apache.HtdigestFile.from_string(self.sample_01)
|
||||
self.assertTrue(ht.delete("user1", "realm"))
|
||||
self.assertTrue(ht.delete("user2", "realm"))
|
||||
self.assertFalse(ht.delete("user5", "realm"))
|
||||
self.assertFalse(ht.delete("user3", "realm5"))
|
||||
self.assertEqual(ht.to_string(), self.sample_02)
|
||||
|
||||
# invalid user
|
||||
self.assertRaises(ValueError, ht.delete, "user:", "realm")
|
||||
|
||||
# invalid realm
|
||||
self.assertRaises(ValueError, ht.delete, "user", "realm:")
|
||||
|
||||
def test_01_delete_autosave(self):
|
||||
path = self.mktemp()
|
||||
set_file(path, self.sample_01)
|
||||
|
||||
ht = apache.HtdigestFile(path)
|
||||
self.assertTrue(ht.delete("user1", "realm"))
|
||||
self.assertFalse(ht.delete("user3", "realm5"))
|
||||
self.assertFalse(ht.delete("user5", "realm"))
|
||||
self.assertEqual(get_file(path), self.sample_01)
|
||||
|
||||
ht.autosave = True
|
||||
self.assertTrue(ht.delete("user2", "realm"))
|
||||
self.assertEqual(get_file(path), self.sample_02)
|
||||
|
||||
def test_02_set_password(self):
|
||||
"""test update()"""
|
||||
ht = apache.HtdigestFile.from_string(self.sample_01)
|
||||
self.assertTrue(ht.set_password("user2", "realm", "pass2x"))
|
||||
self.assertFalse(ht.set_password("user5", "realm", "pass5"))
|
||||
self.assertEqual(ht.to_string(), self.sample_03)
|
||||
|
||||
# default realm
|
||||
self.assertRaises(TypeError, ht.set_password, "user2", "pass3")
|
||||
ht.default_realm = "realm2"
|
||||
ht.set_password("user2", "pass3")
|
||||
ht.check_password("user2", "realm2", "pass3")
|
||||
|
||||
# invalid user
|
||||
self.assertRaises(ValueError, ht.set_password, "user:", "realm", "pass")
|
||||
self.assertRaises(ValueError, ht.set_password, "u"*256, "realm", "pass")
|
||||
|
||||
# invalid realm
|
||||
self.assertRaises(ValueError, ht.set_password, "user", "realm:", "pass")
|
||||
self.assertRaises(ValueError, ht.set_password, "user", "r"*256, "pass")
|
||||
|
||||
# test that legacy update() still works
|
||||
with self.assertWarningList("update\(\) is deprecated"):
|
||||
ht.update("user2", "realm2", "test")
|
||||
self.assertTrue(ht.check_password("user2", "test"))
|
||||
|
||||
# TODO: test set_password autosave
|
||||
|
||||
def test_03_users(self):
|
||||
"""test users()"""
|
||||
ht = apache.HtdigestFile.from_string(self.sample_01)
|
||||
ht.set_password("user5", "realm", "pass5")
|
||||
ht.delete("user3", "realm")
|
||||
ht.set_password("user3", "realm", "pass3")
|
||||
self.assertEqual(sorted(ht.users("realm")), ["user1", "user2", "user3", "user4", "user5"])
|
||||
|
||||
self.assertRaises(TypeError, ht.users, 1)
|
||||
|
||||
def test_04_check_password(self):
|
||||
"""test check_password()"""
|
||||
ht = apache.HtdigestFile.from_string(self.sample_01)
|
||||
self.assertRaises(TypeError, ht.check_password, 1, 'realm', 'pass5')
|
||||
self.assertRaises(TypeError, ht.check_password, 'user', 1, 'pass5')
|
||||
self.assertIs(ht.check_password("user5", "realm","pass5"), None)
|
||||
for i in irange(1,5):
|
||||
i = str(i)
|
||||
self.assertTrue(ht.check_password("user"+i, "realm", "pass"+i))
|
||||
self.assertIs(ht.check_password("user"+i, "realm", "pass5"), False)
|
||||
|
||||
# default realm
|
||||
self.assertRaises(TypeError, ht.check_password, "user5", "pass5")
|
||||
ht.default_realm = "realm"
|
||||
self.assertTrue(ht.check_password("user1", "pass1"))
|
||||
self.assertIs(ht.check_password("user5", "pass5"), None)
|
||||
|
||||
# test that legacy verify() still works
|
||||
with self.assertWarningList(["verify\(\) is deprecated"]*2):
|
||||
self.assertTrue(ht.verify("user1", "realm", "pass1"))
|
||||
self.assertFalse(ht.verify("user1", "realm", "pass2"))
|
||||
|
||||
# invalid user
|
||||
self.assertRaises(ValueError, ht.check_password, "user:", "realm", "pass")
|
||||
|
||||
def test_05_load(self):
|
||||
"""test load()"""
|
||||
# setup empty file
|
||||
path = self.mktemp()
|
||||
set_file(path, "")
|
||||
backdate_file_mtime(path, 5)
|
||||
ha = apache.HtdigestFile(path)
|
||||
self.assertEqual(ha.to_string(), b"")
|
||||
|
||||
# make changes, check load_if_changed() does nothing
|
||||
ha.set_password("user1", "realm", "pass1")
|
||||
ha.load_if_changed()
|
||||
self.assertEqual(ha.to_string(), b'user1:realm:2a6cf53e7d8f8cf39d946dc880b14128\n')
|
||||
|
||||
# change file
|
||||
set_file(path, self.sample_01)
|
||||
ha.load_if_changed()
|
||||
self.assertEqual(ha.to_string(), self.sample_01)
|
||||
|
||||
# make changes, check load_if_changed overwrites them
|
||||
ha.set_password("user5", "realm", "pass5")
|
||||
ha.load()
|
||||
self.assertEqual(ha.to_string(), self.sample_01)
|
||||
|
||||
# test load w/ no path
|
||||
hb = apache.HtdigestFile()
|
||||
self.assertRaises(RuntimeError, hb.load)
|
||||
self.assertRaises(RuntimeError, hb.load_if_changed)
|
||||
|
||||
# test load w/ explicit path
|
||||
hc = apache.HtdigestFile()
|
||||
hc.load(path)
|
||||
self.assertEqual(hc.to_string(), self.sample_01)
|
||||
|
||||
# change file, test deprecated force=False kwd
|
||||
ensure_mtime_changed(path)
|
||||
set_file(path, "")
|
||||
with self.assertWarningList(r"load\(force=False\) is deprecated"):
|
||||
ha.load(force=False)
|
||||
self.assertEqual(ha.to_string(), b"")
|
||||
|
||||
def test_06_save(self):
|
||||
"""test save()"""
|
||||
# load from file
|
||||
path = self.mktemp()
|
||||
set_file(path, self.sample_01)
|
||||
ht = apache.HtdigestFile(path)
|
||||
|
||||
# make changes, check they saved
|
||||
ht.delete("user1", "realm")
|
||||
ht.delete("user2", "realm")
|
||||
ht.save()
|
||||
self.assertEqual(get_file(path), self.sample_02)
|
||||
|
||||
# test save w/ no path
|
||||
hb = apache.HtdigestFile()
|
||||
hb.set_password("user1", "realm", "pass1")
|
||||
self.assertRaises(RuntimeError, hb.save)
|
||||
|
||||
# test save w/ explicit path
|
||||
hb.save(path)
|
||||
self.assertEqual(get_file(path), hb.to_string())
|
||||
|
||||
def test_07_realms(self):
|
||||
"""test realms() & delete_realm()"""
|
||||
ht = apache.HtdigestFile.from_string(self.sample_01)
|
||||
|
||||
self.assertEqual(ht.delete_realm("x"), 0)
|
||||
self.assertEqual(ht.realms(), ['realm'])
|
||||
|
||||
self.assertEqual(ht.delete_realm("realm"), 4)
|
||||
self.assertEqual(ht.realms(), [])
|
||||
self.assertEqual(ht.to_string(), b"")
|
||||
|
||||
def test_08_get_hash(self):
|
||||
"""test get_hash()"""
|
||||
ht = apache.HtdigestFile.from_string(self.sample_01)
|
||||
self.assertEqual(ht.get_hash("user3", "realm"), "a500bb8c02f6a9170ae46af10c898744")
|
||||
self.assertEqual(ht.get_hash("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519")
|
||||
self.assertEqual(ht.get_hash("user5", "realm"), None)
|
||||
|
||||
with self.assertWarningList("find\(\) is deprecated"):
|
||||
self.assertEqual(ht.find("user4", "realm"), "ab7b5d5f28ccc7666315f508c7358519")
|
||||
|
||||
def test_09_encodings(self):
|
||||
"""test encoding parameter"""
|
||||
# test bad encodings cause failure in constructor
|
||||
self.assertRaises(ValueError, apache.HtdigestFile, encoding="utf-16")
|
||||
|
||||
# check sample utf-8
|
||||
ht = apache.HtdigestFile.from_string(self.sample_04_utf8, encoding="utf-8", return_unicode=True)
|
||||
self.assertEqual(ht.realms(), [ u("realm\u00e6") ])
|
||||
self.assertEqual(ht.users(u("realm\u00e6")), [ u("user\u00e6") ])
|
||||
|
||||
# check sample latin-1
|
||||
ht = apache.HtdigestFile.from_string(self.sample_04_latin1, encoding="latin-1", return_unicode=True)
|
||||
self.assertEqual(ht.realms(), [ u("realm\u00e6") ])
|
||||
self.assertEqual(ht.users(u("realm\u00e6")), [ u("user\u00e6") ])
|
||||
|
||||
def test_10_to_string(self):
|
||||
"""test to_string()"""
|
||||
|
||||
# check sample
|
||||
ht = apache.HtdigestFile.from_string(self.sample_01)
|
||||
self.assertEqual(ht.to_string(), self.sample_01)
|
||||
|
||||
# check blank
|
||||
ht = apache.HtdigestFile()
|
||||
self.assertEqual(ht.to_string(), b"")
|
||||
|
||||
def test_11_malformed(self):
|
||||
self.assertRaises(ValueError, apache.HtdigestFile.from_string,
|
||||
b'realm:user1:pass1:other\n')
|
||||
self.assertRaises(ValueError, apache.HtdigestFile.from_string,
|
||||
b'user1:pass1\n')
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
139
venv/lib/python3.12/site-packages/passlib/tests/test_apps.py
Normal file
139
venv/lib/python3.12/site-packages/passlib/tests/test_apps.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""test passlib.apps"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib import apps, hash as hashmod
|
||||
from passlib.tests.utils import TestCase
|
||||
# module
|
||||
|
||||
#=============================================================================
|
||||
# test predefined app contexts
|
||||
#=============================================================================
|
||||
class AppsTest(TestCase):
|
||||
"""perform general tests to make sure contexts work"""
|
||||
# NOTE: these tests are not really comprehensive,
|
||||
# since they would do little but duplicate
|
||||
# the presets in apps.py
|
||||
#
|
||||
# they mainly try to ensure no typos
|
||||
# or dynamic behavior foul-ups.
|
||||
|
||||
def test_master_context(self):
|
||||
ctx = apps.master_context
|
||||
self.assertGreater(len(ctx.schemes()), 50)
|
||||
|
||||
def test_custom_app_context(self):
|
||||
ctx = apps.custom_app_context
|
||||
self.assertEqual(ctx.schemes(), ("sha512_crypt", "sha256_crypt"))
|
||||
for hash in [
|
||||
('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6'
|
||||
'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751'),
|
||||
('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny'
|
||||
'xDGgMlDcOsfaI17'),
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
def test_django16_context(self):
|
||||
ctx = apps.django16_context
|
||||
for hash in [
|
||||
'pbkdf2_sha256$29000$ZsgquwnCyBs2$fBxRQpfKd2PIeMxtkKPy0h7SrnrN+EU/cm67aitoZ2s=',
|
||||
'sha1$0d082$cdb462ae8b6be8784ef24b20778c4d0c82d5957f',
|
||||
'md5$b887a$37767f8a745af10612ad44c80ff52e92',
|
||||
'crypt$95a6d$95x74hLDQKXI2',
|
||||
'098f6bcd4621d373cade4e832627b4f6',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
self.assertEqual(ctx.identify("!"), "django_disabled")
|
||||
self.assertFalse(ctx.verify("test", "!"))
|
||||
|
||||
def test_django_context(self):
|
||||
ctx = apps.django_context
|
||||
for hash in [
|
||||
'pbkdf2_sha256$29000$ZsgquwnCyBs2$fBxRQpfKd2PIeMxtkKPy0h7SrnrN+EU/cm67aitoZ2s=',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
self.assertEqual(ctx.identify("!"), "django_disabled")
|
||||
self.assertFalse(ctx.verify("test", "!"))
|
||||
|
||||
def test_ldap_nocrypt_context(self):
|
||||
ctx = apps.ldap_nocrypt_context
|
||||
for hash in [
|
||||
'{SSHA}cPusOzd6d5n3OjSVK3R329ZGCNyFcC7F',
|
||||
'test',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
self.assertIs(ctx.identify('{CRYPT}$5$rounds=31817$iZGmlyBQ99JSB5'
|
||||
'n6$p4E.pdPBWx19OajgjLRiOW0itGnyxDGgMlDcOsfaI17'), None)
|
||||
|
||||
def test_ldap_context(self):
|
||||
ctx = apps.ldap_context
|
||||
for hash in [
|
||||
('{CRYPT}$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0'
|
||||
'itGnyxDGgMlDcOsfaI17'),
|
||||
'{SSHA}cPusOzd6d5n3OjSVK3R329ZGCNyFcC7F',
|
||||
'test',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
def test_ldap_mysql_context(self):
|
||||
ctx = apps.mysql_context
|
||||
for hash in [
|
||||
'*94BDCEBE19083CE2A1F959FD02F964C7AF4CFC29',
|
||||
'378b243e220ca493',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
def test_postgres_context(self):
|
||||
ctx = apps.postgres_context
|
||||
hash = 'md55d9c68c6c50ed3d02a2fcf54f63993b6'
|
||||
self.assertTrue(ctx.verify("test", hash, user='user'))
|
||||
|
||||
def test_phppass_context(self):
|
||||
ctx = apps.phpass_context
|
||||
for hash in [
|
||||
'$P$8Ja1vJsKa5qyy/b3mCJGXM7GyBnt6..',
|
||||
'$H$8b95CoYQnQ9Y6fSTsACyphNh5yoM02.',
|
||||
'_cD..aBxeRhYFJvtUvsI',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
h1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
|
||||
if hashmod.bcrypt.has_backend():
|
||||
self.assertTrue(ctx.verify("test", h1))
|
||||
self.assertEqual(ctx.default_scheme(), "bcrypt")
|
||||
self.assertEqual(ctx.handler().name, "bcrypt")
|
||||
else:
|
||||
self.assertEqual(ctx.identify(h1), "bcrypt")
|
||||
self.assertEqual(ctx.default_scheme(), "phpass")
|
||||
self.assertEqual(ctx.handler().name, "phpass")
|
||||
|
||||
def test_phpbb3_context(self):
|
||||
ctx = apps.phpbb3_context
|
||||
for hash in [
|
||||
'$P$8Ja1vJsKa5qyy/b3mCJGXM7GyBnt6..',
|
||||
'$H$8b95CoYQnQ9Y6fSTsACyphNh5yoM02.',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
self.assertTrue(ctx.hash("test").startswith("$H$"))
|
||||
|
||||
def test_roundup_context(self):
|
||||
ctx = apps.roundup_context
|
||||
for hash in [
|
||||
'{PBKDF2}9849$JMTYu3eOUSoFYExprVVqbQ$N5.gV.uR1.BTgLSvi0qyPiRlGZ0',
|
||||
'{SHA}a94a8fe5ccb19ba61c4c0873d391e987982fbbd3',
|
||||
'{CRYPT}dptOmKDriOGfU',
|
||||
'{plaintext}test',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
1786
venv/lib/python3.12/site-packages/passlib/tests/test_context.py
Normal file
1786
venv/lib/python3.12/site-packages/passlib/tests/test_context.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,743 @@
|
||||
"""tests for passlib.context
|
||||
|
||||
this file is a clone of the 1.5 test_context.py,
|
||||
containing the tests using the legacy CryptPolicy api.
|
||||
it's being preserved here to ensure the old api doesn't break
|
||||
(until Passlib 1.8, when this and the legacy api will be removed).
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
from logging import getLogger
|
||||
import os
|
||||
import warnings
|
||||
# site
|
||||
try:
|
||||
from pkg_resources import resource_filename
|
||||
except ImportError:
|
||||
resource_filename = None
|
||||
# pkg
|
||||
from passlib import hash
|
||||
from passlib.context import CryptContext, CryptPolicy, LazyCryptContext
|
||||
from passlib.utils import to_bytes, to_unicode
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.tests.utils import TestCase, set_file
|
||||
from passlib.registry import (register_crypt_handler_path,
|
||||
_has_crypt_handler as has_crypt_handler,
|
||||
_unload_handler_name as unload_handler_name,
|
||||
)
|
||||
# module
|
||||
log = getLogger(__name__)
|
||||
|
||||
#=============================================================================
|
||||
#
|
||||
#=============================================================================
|
||||
class CryptPolicyTest(TestCase):
|
||||
"""test CryptPolicy object"""
|
||||
|
||||
# TODO: need to test user categories w/in all this
|
||||
|
||||
descriptionPrefix = "CryptPolicy"
|
||||
|
||||
#===================================================================
|
||||
# sample crypt policies used for testing
|
||||
#===================================================================
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# sample 1 - average config file
|
||||
#---------------------------------------------------------------
|
||||
# NOTE: copy of this is stored in file passlib/tests/sample_config_1s.cfg
|
||||
sample_config_1s = """\
|
||||
[passlib]
|
||||
schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
|
||||
default = md5_crypt
|
||||
all.vary_rounds = 10%%
|
||||
bsdi_crypt.max_rounds = 30000
|
||||
bsdi_crypt.default_rounds = 25000
|
||||
sha512_crypt.max_rounds = 50000
|
||||
sha512_crypt.min_rounds = 40000
|
||||
"""
|
||||
sample_config_1s_path = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), "sample_config_1s.cfg"))
|
||||
if not os.path.exists(sample_config_1s_path) and resource_filename:
|
||||
# in case we're zipped up in an egg.
|
||||
sample_config_1s_path = resource_filename("passlib.tests",
|
||||
"sample_config_1s.cfg")
|
||||
|
||||
# make sure sample_config_1s uses \n linesep - tests rely on this
|
||||
assert sample_config_1s.startswith("[passlib]\nschemes")
|
||||
|
||||
sample_config_1pd = dict(
|
||||
schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
|
||||
default = "md5_crypt",
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
all__vary_rounds = 0.1,
|
||||
bsdi_crypt__max_rounds = 30000,
|
||||
bsdi_crypt__default_rounds = 25000,
|
||||
sha512_crypt__max_rounds = 50000,
|
||||
sha512_crypt__min_rounds = 40000,
|
||||
)
|
||||
|
||||
sample_config_1pid = {
|
||||
"schemes": "des_crypt, md5_crypt, bsdi_crypt, sha512_crypt",
|
||||
"default": "md5_crypt",
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
"all.vary_rounds": 0.1,
|
||||
"bsdi_crypt.max_rounds": 30000,
|
||||
"bsdi_crypt.default_rounds": 25000,
|
||||
"sha512_crypt.max_rounds": 50000,
|
||||
"sha512_crypt.min_rounds": 40000,
|
||||
}
|
||||
|
||||
sample_config_1prd = dict(
|
||||
schemes = [ hash.des_crypt, hash.md5_crypt, hash.bsdi_crypt, hash.sha512_crypt],
|
||||
default = "md5_crypt", # NOTE: passlib <= 1.5 was handler obj.
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
all__vary_rounds = 0.1,
|
||||
bsdi_crypt__max_rounds = 30000,
|
||||
bsdi_crypt__default_rounds = 25000,
|
||||
sha512_crypt__max_rounds = 50000,
|
||||
sha512_crypt__min_rounds = 40000,
|
||||
)
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# sample 2 - partial policy & result of overlay on sample 1
|
||||
#---------------------------------------------------------------
|
||||
sample_config_2s = """\
|
||||
[passlib]
|
||||
bsdi_crypt.min_rounds = 29000
|
||||
bsdi_crypt.max_rounds = 35000
|
||||
bsdi_crypt.default_rounds = 31000
|
||||
sha512_crypt.min_rounds = 45000
|
||||
"""
|
||||
|
||||
sample_config_2pd = dict(
|
||||
# using this to test full replacement of existing options
|
||||
bsdi_crypt__min_rounds = 29000,
|
||||
bsdi_crypt__max_rounds = 35000,
|
||||
bsdi_crypt__default_rounds = 31000,
|
||||
# using this to test partial replacement of existing options
|
||||
sha512_crypt__min_rounds=45000,
|
||||
)
|
||||
|
||||
sample_config_12pd = dict(
|
||||
schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
|
||||
default = "md5_crypt",
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
all__vary_rounds = 0.1,
|
||||
bsdi_crypt__min_rounds = 29000,
|
||||
bsdi_crypt__max_rounds = 35000,
|
||||
bsdi_crypt__default_rounds = 31000,
|
||||
sha512_crypt__max_rounds = 50000,
|
||||
sha512_crypt__min_rounds=45000,
|
||||
)
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# sample 3 - just changing default
|
||||
#---------------------------------------------------------------
|
||||
sample_config_3pd = dict(
|
||||
default="sha512_crypt",
|
||||
)
|
||||
|
||||
sample_config_123pd = dict(
|
||||
schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
|
||||
default = "sha512_crypt",
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
all__vary_rounds = 0.1,
|
||||
bsdi_crypt__min_rounds = 29000,
|
||||
bsdi_crypt__max_rounds = 35000,
|
||||
bsdi_crypt__default_rounds = 31000,
|
||||
sha512_crypt__max_rounds = 50000,
|
||||
sha512_crypt__min_rounds=45000,
|
||||
)
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# sample 4 - category specific
|
||||
#---------------------------------------------------------------
|
||||
sample_config_4s = """
|
||||
[passlib]
|
||||
schemes = sha512_crypt
|
||||
all.vary_rounds = 10%%
|
||||
default.sha512_crypt.max_rounds = 20000
|
||||
admin.all.vary_rounds = 5%%
|
||||
admin.sha512_crypt.max_rounds = 40000
|
||||
"""
|
||||
|
||||
sample_config_4pd = dict(
|
||||
schemes = [ "sha512_crypt" ],
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
all__vary_rounds = 0.1,
|
||||
sha512_crypt__max_rounds = 20000,
|
||||
# NOTE: not maintaining backwards compat for rendering to "5%"
|
||||
admin__all__vary_rounds = 0.05,
|
||||
admin__sha512_crypt__max_rounds = 40000,
|
||||
)
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# sample 5 - to_string & deprecation testing
|
||||
#---------------------------------------------------------------
|
||||
sample_config_5s = sample_config_1s + """\
|
||||
deprecated = des_crypt
|
||||
admin__context__deprecated = des_crypt, bsdi_crypt
|
||||
"""
|
||||
|
||||
sample_config_5pd = sample_config_1pd.copy()
|
||||
sample_config_5pd.update(
|
||||
deprecated = [ "des_crypt" ],
|
||||
admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ],
|
||||
)
|
||||
|
||||
sample_config_5pid = sample_config_1pid.copy()
|
||||
sample_config_5pid.update({
|
||||
"deprecated": "des_crypt",
|
||||
"admin.context.deprecated": "des_crypt, bsdi_crypt",
|
||||
})
|
||||
|
||||
sample_config_5prd = sample_config_1prd.copy()
|
||||
sample_config_5prd.update({
|
||||
# XXX: should deprecated return the actual handlers in this case?
|
||||
# would have to modify how policy stores info, for one.
|
||||
"deprecated": ["des_crypt"],
|
||||
"admin__context__deprecated": ["des_crypt", "bsdi_crypt"],
|
||||
})
|
||||
|
||||
#===================================================================
|
||||
# constructors
|
||||
#===================================================================
|
||||
def setUp(self):
|
||||
TestCase.setUp(self)
|
||||
warnings.filterwarnings("ignore",
|
||||
r"The CryptPolicy class has been deprecated")
|
||||
warnings.filterwarnings("ignore",
|
||||
r"the method.*hash_needs_update.*is deprecated")
|
||||
warnings.filterwarnings("ignore", "The 'all' scheme is deprecated.*")
|
||||
warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd")
|
||||
|
||||
def test_00_constructor(self):
|
||||
"""test CryptPolicy() constructor"""
|
||||
policy = CryptPolicy(**self.sample_config_1pd)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
policy = CryptPolicy(self.sample_config_1pd)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
self.assertRaises(TypeError, CryptPolicy, {}, {})
|
||||
self.assertRaises(TypeError, CryptPolicy, {}, dummy=1)
|
||||
|
||||
# check key with too many separators is rejected
|
||||
self.assertRaises(TypeError, CryptPolicy,
|
||||
schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
|
||||
bad__key__bsdi_crypt__max_rounds = 30000,
|
||||
)
|
||||
|
||||
# check nameless handler rejected
|
||||
class nameless(uh.StaticHandler):
|
||||
name = None
|
||||
self.assertRaises(ValueError, CryptPolicy, schemes=[nameless])
|
||||
|
||||
# check scheme must be name or crypt handler
|
||||
self.assertRaises(TypeError, CryptPolicy, schemes=[uh.StaticHandler])
|
||||
|
||||
# check name conflicts are rejected
|
||||
class dummy_1(uh.StaticHandler):
|
||||
name = 'dummy_1'
|
||||
self.assertRaises(KeyError, CryptPolicy, schemes=[dummy_1, dummy_1])
|
||||
|
||||
# with unknown deprecated value
|
||||
self.assertRaises(KeyError, CryptPolicy,
|
||||
schemes=['des_crypt'],
|
||||
deprecated=['md5_crypt'])
|
||||
|
||||
# with unknown default value
|
||||
self.assertRaises(KeyError, CryptPolicy,
|
||||
schemes=['des_crypt'],
|
||||
default='md5_crypt')
|
||||
|
||||
def test_01_from_path_simple(self):
|
||||
"""test CryptPolicy.from_path() constructor"""
|
||||
# NOTE: this is separate so it can also run under GAE
|
||||
|
||||
# test preset stored in existing file
|
||||
path = self.sample_config_1s_path
|
||||
policy = CryptPolicy.from_path(path)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# test if path missing
|
||||
self.assertRaises(EnvironmentError, CryptPolicy.from_path, path + 'xxx')
|
||||
|
||||
def test_01_from_path(self):
|
||||
"""test CryptPolicy.from_path() constructor with encodings"""
|
||||
path = self.mktemp()
|
||||
|
||||
# test "\n" linesep
|
||||
set_file(path, self.sample_config_1s)
|
||||
policy = CryptPolicy.from_path(path)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# test "\r\n" linesep
|
||||
set_file(path, self.sample_config_1s.replace("\n","\r\n"))
|
||||
policy = CryptPolicy.from_path(path)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# test with custom encoding
|
||||
uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8")
|
||||
set_file(path, uc2)
|
||||
policy = CryptPolicy.from_path(path, encoding="utf-16")
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
def test_02_from_string(self):
|
||||
"""test CryptPolicy.from_string() constructor"""
|
||||
# test "\n" linesep
|
||||
policy = CryptPolicy.from_string(self.sample_config_1s)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# test "\r\n" linesep
|
||||
policy = CryptPolicy.from_string(
|
||||
self.sample_config_1s.replace("\n","\r\n"))
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# test with unicode
|
||||
data = to_unicode(self.sample_config_1s)
|
||||
policy = CryptPolicy.from_string(data)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# test with non-ascii-compatible encoding
|
||||
uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8")
|
||||
policy = CryptPolicy.from_string(uc2, encoding="utf-16")
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# test category specific options
|
||||
policy = CryptPolicy.from_string(self.sample_config_4s)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_4pd)
|
||||
|
||||
def test_03_from_source(self):
|
||||
"""test CryptPolicy.from_source() constructor"""
|
||||
# pass it a path
|
||||
policy = CryptPolicy.from_source(self.sample_config_1s_path)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# pass it a string
|
||||
policy = CryptPolicy.from_source(self.sample_config_1s)
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# pass it a dict (NOTE: make a copy to detect in-place modifications)
|
||||
policy = CryptPolicy.from_source(self.sample_config_1pd.copy())
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# pass it existing policy
|
||||
p2 = CryptPolicy.from_source(policy)
|
||||
self.assertIs(policy, p2)
|
||||
|
||||
# pass it something wrong
|
||||
self.assertRaises(TypeError, CryptPolicy.from_source, 1)
|
||||
self.assertRaises(TypeError, CryptPolicy.from_source, [])
|
||||
|
||||
def test_04_from_sources(self):
|
||||
"""test CryptPolicy.from_sources() constructor"""
|
||||
|
||||
# pass it empty list
|
||||
self.assertRaises(ValueError, CryptPolicy.from_sources, [])
|
||||
|
||||
# pass it one-element list
|
||||
policy = CryptPolicy.from_sources([self.sample_config_1s])
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_1pd)
|
||||
|
||||
# pass multiple sources
|
||||
policy = CryptPolicy.from_sources(
|
||||
[
|
||||
self.sample_config_1s_path,
|
||||
self.sample_config_2s,
|
||||
self.sample_config_3pd,
|
||||
])
|
||||
self.assertEqual(policy.to_dict(), self.sample_config_123pd)
|
||||
|
||||
def test_05_replace(self):
|
||||
"""test CryptPolicy.replace() constructor"""
|
||||
|
||||
p1 = CryptPolicy(**self.sample_config_1pd)
|
||||
|
||||
# check overlaying sample 2
|
||||
p2 = p1.replace(**self.sample_config_2pd)
|
||||
self.assertEqual(p2.to_dict(), self.sample_config_12pd)
|
||||
|
||||
# check repeating overlay makes no change
|
||||
p2b = p2.replace(**self.sample_config_2pd)
|
||||
self.assertEqual(p2b.to_dict(), self.sample_config_12pd)
|
||||
|
||||
# check overlaying sample 3
|
||||
p3 = p2.replace(self.sample_config_3pd)
|
||||
self.assertEqual(p3.to_dict(), self.sample_config_123pd)
|
||||
|
||||
def test_06_forbidden(self):
|
||||
"""test CryptPolicy() forbidden kwds"""
|
||||
|
||||
# salt not allowed to be set
|
||||
self.assertRaises(KeyError, CryptPolicy,
|
||||
schemes=["des_crypt"],
|
||||
des_crypt__salt="xx",
|
||||
)
|
||||
self.assertRaises(KeyError, CryptPolicy,
|
||||
schemes=["des_crypt"],
|
||||
all__salt="xx",
|
||||
)
|
||||
|
||||
# schemes not allowed for category
|
||||
self.assertRaises(KeyError, CryptPolicy,
|
||||
schemes=["des_crypt"],
|
||||
user__context__schemes=["md5_crypt"],
|
||||
)
|
||||
|
||||
#===================================================================
|
||||
# reading
|
||||
#===================================================================
|
||||
def test_10_has_schemes(self):
|
||||
"""test has_schemes() method"""
|
||||
|
||||
p1 = CryptPolicy(**self.sample_config_1pd)
|
||||
self.assertTrue(p1.has_schemes())
|
||||
|
||||
p3 = CryptPolicy(**self.sample_config_3pd)
|
||||
self.assertTrue(not p3.has_schemes())
|
||||
|
||||
def test_11_iter_handlers(self):
|
||||
"""test iter_handlers() method"""
|
||||
|
||||
p1 = CryptPolicy(**self.sample_config_1pd)
|
||||
s = self.sample_config_1prd['schemes']
|
||||
self.assertEqual(list(p1.iter_handlers()), s)
|
||||
|
||||
p3 = CryptPolicy(**self.sample_config_3pd)
|
||||
self.assertEqual(list(p3.iter_handlers()), [])
|
||||
|
||||
def test_12_get_handler(self):
|
||||
"""test get_handler() method"""
|
||||
|
||||
p1 = CryptPolicy(**self.sample_config_1pd)
|
||||
|
||||
# check by name
|
||||
self.assertIs(p1.get_handler("bsdi_crypt"), hash.bsdi_crypt)
|
||||
|
||||
# check by missing name
|
||||
self.assertIs(p1.get_handler("sha256_crypt"), None)
|
||||
self.assertRaises(KeyError, p1.get_handler, "sha256_crypt", required=True)
|
||||
|
||||
# check default
|
||||
self.assertIs(p1.get_handler(), hash.md5_crypt)
|
||||
|
||||
def test_13_get_options(self):
|
||||
"""test get_options() method"""
|
||||
|
||||
p12 = CryptPolicy(**self.sample_config_12pd)
|
||||
|
||||
self.assertEqual(p12.get_options("bsdi_crypt"),dict(
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
vary_rounds = 0.1,
|
||||
min_rounds = 29000,
|
||||
max_rounds = 35000,
|
||||
default_rounds = 31000,
|
||||
))
|
||||
|
||||
self.assertEqual(p12.get_options("sha512_crypt"),dict(
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
vary_rounds = 0.1,
|
||||
min_rounds = 45000,
|
||||
max_rounds = 50000,
|
||||
))
|
||||
|
||||
p4 = CryptPolicy.from_string(self.sample_config_4s)
|
||||
self.assertEqual(p4.get_options("sha512_crypt"), dict(
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
vary_rounds=0.1,
|
||||
max_rounds=20000,
|
||||
))
|
||||
|
||||
self.assertEqual(p4.get_options("sha512_crypt", "user"), dict(
|
||||
# NOTE: not maintaining backwards compat for rendering to "10%"
|
||||
vary_rounds=0.1,
|
||||
max_rounds=20000,
|
||||
))
|
||||
|
||||
self.assertEqual(p4.get_options("sha512_crypt", "admin"), dict(
|
||||
# NOTE: not maintaining backwards compat for rendering to "5%"
|
||||
vary_rounds=0.05,
|
||||
max_rounds=40000,
|
||||
))
|
||||
|
||||
def test_14_handler_is_deprecated(self):
|
||||
"""test handler_is_deprecated() method"""
|
||||
pa = CryptPolicy(**self.sample_config_1pd)
|
||||
pb = CryptPolicy(**self.sample_config_5pd)
|
||||
|
||||
self.assertFalse(pa.handler_is_deprecated("des_crypt"))
|
||||
self.assertFalse(pa.handler_is_deprecated(hash.bsdi_crypt))
|
||||
self.assertFalse(pa.handler_is_deprecated("sha512_crypt"))
|
||||
|
||||
self.assertTrue(pb.handler_is_deprecated("des_crypt"))
|
||||
self.assertFalse(pb.handler_is_deprecated(hash.bsdi_crypt))
|
||||
self.assertFalse(pb.handler_is_deprecated("sha512_crypt"))
|
||||
|
||||
# check categories as well
|
||||
self.assertTrue(pb.handler_is_deprecated("des_crypt", "user"))
|
||||
self.assertFalse(pb.handler_is_deprecated("bsdi_crypt", "user"))
|
||||
self.assertTrue(pb.handler_is_deprecated("des_crypt", "admin"))
|
||||
self.assertTrue(pb.handler_is_deprecated("bsdi_crypt", "admin"))
|
||||
|
||||
# check deprecation is overridden per category
|
||||
pc = CryptPolicy(
|
||||
schemes=["md5_crypt", "des_crypt"],
|
||||
deprecated=["md5_crypt"],
|
||||
user__context__deprecated=["des_crypt"],
|
||||
)
|
||||
self.assertTrue(pc.handler_is_deprecated("md5_crypt"))
|
||||
self.assertFalse(pc.handler_is_deprecated("des_crypt"))
|
||||
self.assertFalse(pc.handler_is_deprecated("md5_crypt", "user"))
|
||||
self.assertTrue(pc.handler_is_deprecated("des_crypt", "user"))
|
||||
|
||||
def test_15_min_verify_time(self):
|
||||
"""test get_min_verify_time() method"""
|
||||
# silence deprecation warnings for min verify time
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
|
||||
pa = CryptPolicy()
|
||||
self.assertEqual(pa.get_min_verify_time(), 0)
|
||||
self.assertEqual(pa.get_min_verify_time('admin'), 0)
|
||||
|
||||
pb = pa.replace(min_verify_time=.1)
|
||||
self.assertEqual(pb.get_min_verify_time(), 0)
|
||||
self.assertEqual(pb.get_min_verify_time('admin'), 0)
|
||||
|
||||
#===================================================================
|
||||
# serialization
|
||||
#===================================================================
|
||||
def test_20_iter_config(self):
|
||||
"""test iter_config() method"""
|
||||
p5 = CryptPolicy(**self.sample_config_5pd)
|
||||
self.assertEqual(dict(p5.iter_config()), self.sample_config_5pd)
|
||||
self.assertEqual(dict(p5.iter_config(resolve=True)), self.sample_config_5prd)
|
||||
self.assertEqual(dict(p5.iter_config(ini=True)), self.sample_config_5pid)
|
||||
|
||||
def test_21_to_dict(self):
|
||||
"""test to_dict() method"""
|
||||
p5 = CryptPolicy(**self.sample_config_5pd)
|
||||
self.assertEqual(p5.to_dict(), self.sample_config_5pd)
|
||||
self.assertEqual(p5.to_dict(resolve=True), self.sample_config_5prd)
|
||||
|
||||
def test_22_to_string(self):
|
||||
"""test to_string() method"""
|
||||
pa = CryptPolicy(**self.sample_config_5pd)
|
||||
s = pa.to_string() # NOTE: can't compare string directly, ordering etc may not match
|
||||
pb = CryptPolicy.from_string(s)
|
||||
self.assertEqual(pb.to_dict(), self.sample_config_5pd)
|
||||
|
||||
s = pa.to_string(encoding="latin-1")
|
||||
self.assertIsInstance(s, bytes)
|
||||
|
||||
#===================================================================
|
||||
#
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# CryptContext
|
||||
#=============================================================================
|
||||
class CryptContextTest(TestCase):
|
||||
"""test CryptContext class"""
|
||||
descriptionPrefix = "CryptContext"
|
||||
|
||||
def setUp(self):
|
||||
TestCase.setUp(self)
|
||||
warnings.filterwarnings("ignore",
|
||||
r"CryptContext\(\)\.replace\(\) has been deprecated.*")
|
||||
warnings.filterwarnings("ignore",
|
||||
r"The CryptContext ``policy`` keyword has been deprecated.*")
|
||||
warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*")
|
||||
warnings.filterwarnings("ignore",
|
||||
r"the method.*hash_needs_update.*is deprecated")
|
||||
|
||||
#===================================================================
|
||||
# constructor
|
||||
#===================================================================
|
||||
def test_00_constructor(self):
|
||||
"""test constructor"""
|
||||
# create crypt context using handlers
|
||||
cc = CryptContext([hash.md5_crypt, hash.bsdi_crypt, hash.des_crypt])
|
||||
c,b,a = cc.policy.iter_handlers()
|
||||
self.assertIs(a, hash.des_crypt)
|
||||
self.assertIs(b, hash.bsdi_crypt)
|
||||
self.assertIs(c, hash.md5_crypt)
|
||||
|
||||
# create context using names
|
||||
cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"])
|
||||
c,b,a = cc.policy.iter_handlers()
|
||||
self.assertIs(a, hash.des_crypt)
|
||||
self.assertIs(b, hash.bsdi_crypt)
|
||||
self.assertIs(c, hash.md5_crypt)
|
||||
|
||||
# policy kwd
|
||||
policy = cc.policy
|
||||
cc = CryptContext(policy=policy)
|
||||
self.assertEqual(cc.to_dict(), policy.to_dict())
|
||||
|
||||
cc = CryptContext(policy=policy, default="bsdi_crypt")
|
||||
self.assertNotEqual(cc.to_dict(), policy.to_dict())
|
||||
self.assertEqual(cc.to_dict(), dict(schemes=["md5_crypt","bsdi_crypt","des_crypt"],
|
||||
default="bsdi_crypt"))
|
||||
|
||||
self.assertRaises(TypeError, setattr, cc, 'policy', None)
|
||||
self.assertRaises(TypeError, CryptContext, policy='x')
|
||||
|
||||
def test_01_replace(self):
|
||||
"""test replace()"""
|
||||
|
||||
cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"])
|
||||
self.assertIs(cc.policy.get_handler(), hash.md5_crypt)
|
||||
|
||||
cc2 = cc.replace()
|
||||
self.assertIsNot(cc2, cc)
|
||||
# NOTE: was not able to maintain backward compatibility with this...
|
||||
##self.assertIs(cc2.policy, cc.policy)
|
||||
|
||||
cc3 = cc.replace(default="bsdi_crypt")
|
||||
self.assertIsNot(cc3, cc)
|
||||
# NOTE: was not able to maintain backward compatibility with this...
|
||||
##self.assertIs(cc3.policy, cc.policy)
|
||||
self.assertIs(cc3.policy.get_handler(), hash.bsdi_crypt)
|
||||
|
||||
def test_02_no_handlers(self):
|
||||
"""test no handlers"""
|
||||
|
||||
# check constructor...
|
||||
cc = CryptContext()
|
||||
self.assertRaises(KeyError, cc.identify, 'hash', required=True)
|
||||
self.assertRaises(KeyError, cc.hash, 'secret')
|
||||
self.assertRaises(KeyError, cc.verify, 'secret', 'hash')
|
||||
|
||||
# check updating policy after the fact...
|
||||
cc = CryptContext(['md5_crypt'])
|
||||
p = CryptPolicy(schemes=[])
|
||||
cc.policy = p
|
||||
|
||||
self.assertRaises(KeyError, cc.identify, 'hash', required=True)
|
||||
self.assertRaises(KeyError, cc.hash, 'secret')
|
||||
self.assertRaises(KeyError, cc.verify, 'secret', 'hash')
|
||||
|
||||
#===================================================================
|
||||
# policy adaptation
|
||||
#===================================================================
|
||||
sample_policy_1 = dict(
|
||||
schemes = [ "des_crypt", "md5_crypt", "phpass", "bsdi_crypt",
|
||||
"sha256_crypt"],
|
||||
deprecated = [ "des_crypt", ],
|
||||
default = "sha256_crypt",
|
||||
bsdi_crypt__max_rounds = 30,
|
||||
bsdi_crypt__default_rounds = 25,
|
||||
bsdi_crypt__vary_rounds = 0,
|
||||
sha256_crypt__max_rounds = 3000,
|
||||
sha256_crypt__min_rounds = 2000,
|
||||
sha256_crypt__default_rounds = 3000,
|
||||
phpass__ident = "H",
|
||||
phpass__default_rounds = 7,
|
||||
)
|
||||
|
||||
def test_12_hash_needs_update(self):
|
||||
"""test hash_needs_update() method"""
|
||||
cc = CryptContext(**self.sample_policy_1)
|
||||
|
||||
# check deprecated scheme
|
||||
self.assertTrue(cc.hash_needs_update('9XXD4trGYeGJA'))
|
||||
self.assertFalse(cc.hash_needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0'))
|
||||
|
||||
# check min rounds
|
||||
self.assertTrue(cc.hash_needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/'))
|
||||
self.assertFalse(cc.hash_needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8'))
|
||||
|
||||
# check max rounds
|
||||
self.assertFalse(cc.hash_needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.'))
|
||||
self.assertTrue(cc.hash_needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA'))
|
||||
|
||||
#===================================================================
|
||||
# border cases
|
||||
#===================================================================
|
||||
def test_30_nonstring_hash(self):
|
||||
"""test non-string hash values cause error"""
|
||||
warnings.filterwarnings("ignore", ".*needs_update.*'scheme' keyword is deprecated.*")
|
||||
|
||||
#
|
||||
# test hash=None or some other non-string causes TypeError
|
||||
# and that explicit-scheme code path behaves the same.
|
||||
#
|
||||
cc = CryptContext(["des_crypt"])
|
||||
for hash, kwds in [
|
||||
(None, {}),
|
||||
# NOTE: 'scheme' kwd is deprecated...
|
||||
(None, {"scheme": "des_crypt"}),
|
||||
(1, {}),
|
||||
((), {}),
|
||||
]:
|
||||
|
||||
self.assertRaises(TypeError, cc.hash_needs_update, hash, **kwds)
|
||||
|
||||
cc2 = CryptContext(["mysql323"])
|
||||
self.assertRaises(TypeError, cc2.hash_needs_update, None)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# LazyCryptContext
|
||||
#=============================================================================
|
||||
class dummy_2(uh.StaticHandler):
|
||||
name = "dummy_2"
|
||||
|
||||
class LazyCryptContextTest(TestCase):
|
||||
descriptionPrefix = "LazyCryptContext"
|
||||
|
||||
def setUp(self):
|
||||
TestCase.setUp(self)
|
||||
|
||||
# make sure this isn't registered before OR after
|
||||
unload_handler_name("dummy_2")
|
||||
self.addCleanup(unload_handler_name, "dummy_2")
|
||||
|
||||
# silence some warnings
|
||||
warnings.filterwarnings("ignore",
|
||||
r"CryptContext\(\)\.replace\(\) has been deprecated")
|
||||
warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*")
|
||||
|
||||
def test_kwd_constructor(self):
|
||||
"""test plain kwds"""
|
||||
self.assertFalse(has_crypt_handler("dummy_2"))
|
||||
register_crypt_handler_path("dummy_2", "passlib.tests.test_context")
|
||||
|
||||
cc = LazyCryptContext(iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"])
|
||||
|
||||
self.assertFalse(has_crypt_handler("dummy_2", True))
|
||||
|
||||
self.assertTrue(cc.policy.handler_is_deprecated("des_crypt"))
|
||||
self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"])
|
||||
|
||||
self.assertTrue(has_crypt_handler("dummy_2", True))
|
||||
|
||||
def test_callable_constructor(self):
|
||||
"""test create_policy() hook, returning CryptPolicy"""
|
||||
self.assertFalse(has_crypt_handler("dummy_2"))
|
||||
register_crypt_handler_path("dummy_2", "passlib.tests.test_context")
|
||||
|
||||
def create_policy(flag=False):
|
||||
self.assertTrue(flag)
|
||||
return CryptPolicy(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"])
|
||||
|
||||
cc = LazyCryptContext(create_policy=create_policy, flag=True)
|
||||
|
||||
self.assertFalse(has_crypt_handler("dummy_2", True))
|
||||
|
||||
self.assertTrue(cc.policy.handler_is_deprecated("des_crypt"))
|
||||
self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"])
|
||||
|
||||
self.assertTrue(has_crypt_handler("dummy_2", True))
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,160 @@
|
||||
"""passlib.tests -- unittests for passlib.crypto._md4"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement, division
|
||||
# core
|
||||
from binascii import hexlify
|
||||
import hashlib
|
||||
# site
|
||||
# pkg
|
||||
# module
|
||||
from passlib.utils.compat import bascii_to_str, PY3, u
|
||||
from passlib.crypto.digest import lookup_hash
|
||||
from passlib.tests.utils import TestCase, skipUnless
|
||||
# local
|
||||
__all__ = [
|
||||
"_Common_MD4_Test",
|
||||
"MD4_Builtin_Test",
|
||||
"MD4_SSL_Test",
|
||||
]
|
||||
#=============================================================================
|
||||
# test pure-python MD4 implementation
|
||||
#=============================================================================
|
||||
class _Common_MD4_Test(TestCase):
|
||||
"""common code for testing md4 backends"""
|
||||
|
||||
vectors = [
|
||||
# input -> hex digest
|
||||
# test vectors from http://www.faqs.org/rfcs/rfc1320.html - A.5
|
||||
(b"", "31d6cfe0d16ae931b73c59d7e0c089c0"),
|
||||
(b"a", "bde52cb31de33e46245e05fbdbd6fb24"),
|
||||
(b"abc", "a448017aaf21d8525fc10ae87aa6729d"),
|
||||
(b"message digest", "d9130a8164549fe818874806e1c7014b"),
|
||||
(b"abcdefghijklmnopqrstuvwxyz", "d79e1c308aa5bbcdeea8ed63df412da9"),
|
||||
(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "043f8582f241db351ce627e153e7f0e4"),
|
||||
(b"12345678901234567890123456789012345678901234567890123456789012345678901234567890", "e33b4ddc9c38f2199c3e7b164fcc0536"),
|
||||
]
|
||||
|
||||
def get_md4_const(self):
|
||||
"""
|
||||
get md4 constructor --
|
||||
overridden by subclasses to use alternate backends.
|
||||
"""
|
||||
return lookup_hash("md4").const
|
||||
|
||||
def test_attrs(self):
|
||||
"""informational attributes"""
|
||||
h = self.get_md4_const()()
|
||||
self.assertEqual(h.name, "md4")
|
||||
self.assertEqual(h.digest_size, 16)
|
||||
self.assertEqual(h.block_size, 64)
|
||||
|
||||
def test_md4_update(self):
|
||||
"""update() method"""
|
||||
md4 = self.get_md4_const()
|
||||
h = md4(b'')
|
||||
self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0")
|
||||
|
||||
h.update(b'a')
|
||||
self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24")
|
||||
|
||||
h.update(b'bcdefghijklmnopqrstuvwxyz')
|
||||
self.assertEqual(h.hexdigest(), "d79e1c308aa5bbcdeea8ed63df412da9")
|
||||
|
||||
if PY3:
|
||||
# reject unicode, hash should return digest of b''
|
||||
h = md4()
|
||||
self.assertRaises(TypeError, h.update, u('a'))
|
||||
self.assertEqual(h.hexdigest(), "31d6cfe0d16ae931b73c59d7e0c089c0")
|
||||
else:
|
||||
# coerce unicode to ascii, hash should return digest of b'a'
|
||||
h = md4()
|
||||
h.update(u('a'))
|
||||
self.assertEqual(h.hexdigest(), "bde52cb31de33e46245e05fbdbd6fb24")
|
||||
|
||||
def test_md4_hexdigest(self):
|
||||
"""hexdigest() method"""
|
||||
md4 = self.get_md4_const()
|
||||
for input, hex in self.vectors:
|
||||
out = md4(input).hexdigest()
|
||||
self.assertEqual(out, hex)
|
||||
|
||||
def test_md4_digest(self):
|
||||
"""digest() method"""
|
||||
md4 = self.get_md4_const()
|
||||
for input, hex in self.vectors:
|
||||
out = bascii_to_str(hexlify(md4(input).digest()))
|
||||
self.assertEqual(out, hex)
|
||||
|
||||
def test_md4_copy(self):
|
||||
"""copy() method"""
|
||||
md4 = self.get_md4_const()
|
||||
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')
|
||||
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# create subclasses to test various backends
|
||||
#------------------------------------------------------------------------
|
||||
|
||||
def has_native_md4(): # pragma: no cover -- runtime detection
|
||||
"""
|
||||
check if hashlib natively supports md4.
|
||||
"""
|
||||
try:
|
||||
hashlib.new("md4")
|
||||
return True
|
||||
except ValueError:
|
||||
# not supported - ssl probably missing (e.g. ironpython)
|
||||
return False
|
||||
|
||||
|
||||
@skipUnless(has_native_md4(), "hashlib lacks ssl/md4 support")
|
||||
class MD4_SSL_Test(_Common_MD4_Test):
|
||||
descriptionPrefix = "hashlib.new('md4')"
|
||||
|
||||
# NOTE: we trust ssl got md4 implementation right,
|
||||
# this is more to test our test is correct :)
|
||||
|
||||
def setUp(self):
|
||||
super(MD4_SSL_Test, self).setUp()
|
||||
|
||||
# make sure we're using right constructor.
|
||||
self.assertEqual(self.get_md4_const().__module__, "hashlib")
|
||||
|
||||
|
||||
class MD4_Builtin_Test(_Common_MD4_Test):
|
||||
descriptionPrefix = "passlib.crypto._md4.md4()"
|
||||
|
||||
def setUp(self):
|
||||
super(MD4_Builtin_Test, self).setUp()
|
||||
|
||||
if has_native_md4():
|
||||
|
||||
# Temporarily make lookup_hash() use builtin pure-python implementation,
|
||||
# by monkeypatching hashlib.new() to ensure we fall back to passlib's md4 class.
|
||||
orig = hashlib.new
|
||||
def wrapper(name, *args):
|
||||
if name == "md4":
|
||||
raise ValueError("md4 disabled for testing")
|
||||
return orig(name, *args)
|
||||
self.patchAttr(hashlib, "new", wrapper)
|
||||
|
||||
# flush cache before & after test, since we're mucking with it.
|
||||
lookup_hash.clear_cache()
|
||||
self.addCleanup(lookup_hash.clear_cache)
|
||||
|
||||
# make sure we're using right constructor.
|
||||
self.assertEqual(self.get_md4_const().__module__, "passlib.crypto._md4")
|
||||
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,194 @@
|
||||
"""passlib.tests -- unittests for passlib.crypto.des"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement, division
|
||||
# core
|
||||
from functools import partial
|
||||
# site
|
||||
# pkg
|
||||
# module
|
||||
from passlib.utils import getrandbytes
|
||||
from passlib.tests.utils import TestCase
|
||||
|
||||
#=============================================================================
|
||||
# test DES routines
|
||||
#=============================================================================
|
||||
class DesTest(TestCase):
|
||||
descriptionPrefix = "passlib.crypto.des"
|
||||
|
||||
# test vectors taken from http://www.skepticfiles.org/faq/testdes.htm
|
||||
des_test_vectors = [
|
||||
# key, plaintext, ciphertext
|
||||
(0x0000000000000000, 0x0000000000000000, 0x8CA64DE9C1B123A7),
|
||||
(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0x7359B2163E4EDC58),
|
||||
(0x3000000000000000, 0x1000000000000001, 0x958E6E627A05557B),
|
||||
(0x1111111111111111, 0x1111111111111111, 0xF40379AB9E0EC533),
|
||||
(0x0123456789ABCDEF, 0x1111111111111111, 0x17668DFC7292532D),
|
||||
(0x1111111111111111, 0x0123456789ABCDEF, 0x8A5AE1F81AB8F2DD),
|
||||
(0x0000000000000000, 0x0000000000000000, 0x8CA64DE9C1B123A7),
|
||||
(0xFEDCBA9876543210, 0x0123456789ABCDEF, 0xED39D950FA74BCC4),
|
||||
(0x7CA110454A1A6E57, 0x01A1D6D039776742, 0x690F5B0D9A26939B),
|
||||
(0x0131D9619DC1376E, 0x5CD54CA83DEF57DA, 0x7A389D10354BD271),
|
||||
(0x07A1133E4A0B2686, 0x0248D43806F67172, 0x868EBB51CAB4599A),
|
||||
(0x3849674C2602319E, 0x51454B582DDF440A, 0x7178876E01F19B2A),
|
||||
(0x04B915BA43FEB5B6, 0x42FD443059577FA2, 0xAF37FB421F8C4095),
|
||||
(0x0113B970FD34F2CE, 0x059B5E0851CF143A, 0x86A560F10EC6D85B),
|
||||
(0x0170F175468FB5E6, 0x0756D8E0774761D2, 0x0CD3DA020021DC09),
|
||||
(0x43297FAD38E373FE, 0x762514B829BF486A, 0xEA676B2CB7DB2B7A),
|
||||
(0x07A7137045DA2A16, 0x3BDD119049372802, 0xDFD64A815CAF1A0F),
|
||||
(0x04689104C2FD3B2F, 0x26955F6835AF609A, 0x5C513C9C4886C088),
|
||||
(0x37D06BB516CB7546, 0x164D5E404F275232, 0x0A2AEEAE3FF4AB77),
|
||||
(0x1F08260D1AC2465E, 0x6B056E18759F5CCA, 0xEF1BF03E5DFA575A),
|
||||
(0x584023641ABA6176, 0x004BD6EF09176062, 0x88BF0DB6D70DEE56),
|
||||
(0x025816164629B007, 0x480D39006EE762F2, 0xA1F9915541020B56),
|
||||
(0x49793EBC79B3258F, 0x437540C8698F3CFA, 0x6FBF1CAFCFFD0556),
|
||||
(0x4FB05E1515AB73A7, 0x072D43A077075292, 0x2F22E49BAB7CA1AC),
|
||||
(0x49E95D6D4CA229BF, 0x02FE55778117F12A, 0x5A6B612CC26CCE4A),
|
||||
(0x018310DC409B26D6, 0x1D9D5C5018F728C2, 0x5F4C038ED12B2E41),
|
||||
(0x1C587F1C13924FEF, 0x305532286D6F295A, 0x63FAC0D034D9F793),
|
||||
(0x0101010101010101, 0x0123456789ABCDEF, 0x617B3A0CE8F07100),
|
||||
(0x1F1F1F1F0E0E0E0E, 0x0123456789ABCDEF, 0xDB958605F8C8C606),
|
||||
(0xE0FEE0FEF1FEF1FE, 0x0123456789ABCDEF, 0xEDBFD1C66C29CCC7),
|
||||
(0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x355550B2150E2451),
|
||||
(0xFFFFFFFFFFFFFFFF, 0x0000000000000000, 0xCAAAAF4DEAF1DBAE),
|
||||
(0x0123456789ABCDEF, 0x0000000000000000, 0xD5D44FF720683D0D),
|
||||
(0xFEDCBA9876543210, 0xFFFFFFFFFFFFFFFF, 0x2A2BB008DF97C2F2),
|
||||
]
|
||||
|
||||
def test_01_expand(self):
|
||||
"""expand_des_key()"""
|
||||
from passlib.crypto.des import expand_des_key, shrink_des_key, \
|
||||
_KDATA_MASK, INT_56_MASK
|
||||
|
||||
# make sure test vectors are preserved (sans parity bits)
|
||||
# uses ints, bytes are tested under # 02
|
||||
for key1, _, _ in self.des_test_vectors:
|
||||
key2 = shrink_des_key(key1)
|
||||
key3 = expand_des_key(key2)
|
||||
# NOTE: this assumes expand_des_key() sets parity bits to 0
|
||||
self.assertEqual(key3, key1 & _KDATA_MASK)
|
||||
|
||||
# type checks
|
||||
self.assertRaises(TypeError, expand_des_key, 1.0)
|
||||
|
||||
# too large
|
||||
self.assertRaises(ValueError, expand_des_key, INT_56_MASK+1)
|
||||
self.assertRaises(ValueError, expand_des_key, b"\x00"*8)
|
||||
|
||||
# too small
|
||||
self.assertRaises(ValueError, expand_des_key, -1)
|
||||
self.assertRaises(ValueError, expand_des_key, b"\x00"*6)
|
||||
|
||||
def test_02_shrink(self):
|
||||
"""shrink_des_key()"""
|
||||
from passlib.crypto.des import expand_des_key, shrink_des_key, INT_64_MASK
|
||||
rng = self.getRandom()
|
||||
|
||||
# make sure reverse works for some random keys
|
||||
# uses bytes, ints are tested under # 01
|
||||
for i in range(20):
|
||||
key1 = getrandbytes(rng, 7)
|
||||
key2 = expand_des_key(key1)
|
||||
key3 = shrink_des_key(key2)
|
||||
self.assertEqual(key3, key1)
|
||||
|
||||
# type checks
|
||||
self.assertRaises(TypeError, shrink_des_key, 1.0)
|
||||
|
||||
# too large
|
||||
self.assertRaises(ValueError, shrink_des_key, INT_64_MASK+1)
|
||||
self.assertRaises(ValueError, shrink_des_key, b"\x00"*9)
|
||||
|
||||
# too small
|
||||
self.assertRaises(ValueError, shrink_des_key, -1)
|
||||
self.assertRaises(ValueError, shrink_des_key, b"\x00"*7)
|
||||
|
||||
def _random_parity(self, key):
|
||||
"""randomize parity bits"""
|
||||
from passlib.crypto.des import _KDATA_MASK, _KPARITY_MASK, INT_64_MASK
|
||||
rng = self.getRandom()
|
||||
return (key & _KDATA_MASK) | (rng.randint(0,INT_64_MASK) & _KPARITY_MASK)
|
||||
|
||||
def test_03_encrypt_bytes(self):
|
||||
"""des_encrypt_block()"""
|
||||
from passlib.crypto.des import (des_encrypt_block, shrink_des_key,
|
||||
_pack64, _unpack64)
|
||||
|
||||
# run through test vectors
|
||||
for key, plaintext, correct in self.des_test_vectors:
|
||||
# convert to bytes
|
||||
key = _pack64(key)
|
||||
plaintext = _pack64(plaintext)
|
||||
correct = _pack64(correct)
|
||||
|
||||
# test 64-bit key
|
||||
result = des_encrypt_block(key, plaintext)
|
||||
self.assertEqual(result, correct, "key=%r plaintext=%r:" %
|
||||
(key, plaintext))
|
||||
|
||||
# test 56-bit version
|
||||
key2 = shrink_des_key(key)
|
||||
result = des_encrypt_block(key2, plaintext)
|
||||
self.assertEqual(result, correct, "key=%r shrink(key)=%r plaintext=%r:" %
|
||||
(key, key2, plaintext))
|
||||
|
||||
# test with random parity bits
|
||||
for _ in range(20):
|
||||
key3 = _pack64(self._random_parity(_unpack64(key)))
|
||||
result = des_encrypt_block(key3, plaintext)
|
||||
self.assertEqual(result, correct, "key=%r rndparity(key)=%r plaintext=%r:" %
|
||||
(key, key3, plaintext))
|
||||
|
||||
# check invalid keys
|
||||
stub = b'\x00' * 8
|
||||
self.assertRaises(TypeError, des_encrypt_block, 0, stub)
|
||||
self.assertRaises(ValueError, des_encrypt_block, b'\x00'*6, stub)
|
||||
|
||||
# check invalid input
|
||||
self.assertRaises(TypeError, des_encrypt_block, stub, 0)
|
||||
self.assertRaises(ValueError, des_encrypt_block, stub, b'\x00'*7)
|
||||
|
||||
# check invalid salts
|
||||
self.assertRaises(ValueError, des_encrypt_block, stub, stub, salt=-1)
|
||||
self.assertRaises(ValueError, des_encrypt_block, stub, stub, salt=1<<24)
|
||||
|
||||
# check invalid rounds
|
||||
self.assertRaises(ValueError, des_encrypt_block, stub, stub, 0, rounds=0)
|
||||
|
||||
def test_04_encrypt_ints(self):
|
||||
"""des_encrypt_int_block()"""
|
||||
from passlib.crypto.des import des_encrypt_int_block
|
||||
|
||||
# run through test vectors
|
||||
for key, plaintext, correct in self.des_test_vectors:
|
||||
# test 64-bit key
|
||||
result = des_encrypt_int_block(key, plaintext)
|
||||
self.assertEqual(result, correct, "key=%r plaintext=%r:" %
|
||||
(key, plaintext))
|
||||
|
||||
# test with random parity bits
|
||||
for _ in range(20):
|
||||
key3 = self._random_parity(key)
|
||||
result = des_encrypt_int_block(key3, plaintext)
|
||||
self.assertEqual(result, correct, "key=%r rndparity(key)=%r plaintext=%r:" %
|
||||
(key, key3, plaintext))
|
||||
|
||||
# check invalid keys
|
||||
self.assertRaises(TypeError, des_encrypt_int_block, b'\x00', 0)
|
||||
self.assertRaises(ValueError, des_encrypt_int_block, -1, 0)
|
||||
|
||||
# check invalid input
|
||||
self.assertRaises(TypeError, des_encrypt_int_block, 0, b'\x00')
|
||||
self.assertRaises(ValueError, des_encrypt_int_block, 0, -1)
|
||||
|
||||
# check invalid salts
|
||||
self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, salt=-1)
|
||||
self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, salt=1<<24)
|
||||
|
||||
# check invalid rounds
|
||||
self.assertRaises(ValueError, des_encrypt_int_block, 0, 0, 0, rounds=0)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,544 @@
|
||||
"""tests for passlib.utils.(des|pbkdf2|md4)"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement, division
|
||||
# core
|
||||
from binascii import hexlify
|
||||
import hashlib
|
||||
import warnings
|
||||
# site
|
||||
# pkg
|
||||
# module
|
||||
from passlib.exc import UnknownHashError
|
||||
from passlib.utils.compat import PY3, u, JYTHON
|
||||
from passlib.tests.utils import TestCase, TEST_MODE, skipUnless, hb
|
||||
|
||||
#=============================================================================
|
||||
# test assorted crypto helpers
|
||||
#=============================================================================
|
||||
class HashInfoTest(TestCase):
|
||||
"""test various crypto functions"""
|
||||
descriptionPrefix = "passlib.crypto.digest"
|
||||
|
||||
#: list of formats norm_hash_name() should support
|
||||
norm_hash_formats = ["hashlib", "iana"]
|
||||
|
||||
#: test cases for norm_hash_name()
|
||||
#: each row contains (iana name, hashlib name, ... 0+ unnormalized names)
|
||||
norm_hash_samples = [
|
||||
# real hashes
|
||||
("md5", "md5", "SCRAM-MD5-PLUS", "MD-5"),
|
||||
("sha1", "sha-1", "SCRAM-SHA-1", "SHA1"),
|
||||
("sha256", "sha-256", "SHA_256", "sha2-256"),
|
||||
("ripemd160", "ripemd-160", "SCRAM-RIPEMD-160", "RIPEmd160",
|
||||
# NOTE: there was an older "RIPEMD" & "RIPEMD-128", but python treates "RIPEMD"
|
||||
# as alias for "RIPEMD-160"
|
||||
"ripemd", "SCRAM-RIPEMD"),
|
||||
|
||||
# fake hashes (to check if fallback normalization behaves sanely)
|
||||
("sha4_256", "sha4-256", "SHA4-256", "SHA-4-256"),
|
||||
("test128", "test-128", "TEST128"),
|
||||
("test2", "test2", "TEST-2"),
|
||||
("test3_128", "test3-128", "TEST-3-128"),
|
||||
]
|
||||
|
||||
def test_norm_hash_name(self):
|
||||
"""norm_hash_name()"""
|
||||
from itertools import chain
|
||||
from passlib.crypto.digest import norm_hash_name, _known_hash_names
|
||||
|
||||
# snapshot warning state, ignore unknown hash warnings
|
||||
ctx = warnings.catch_warnings()
|
||||
ctx.__enter__()
|
||||
self.addCleanup(ctx.__exit__)
|
||||
warnings.filterwarnings("ignore", '.*unknown hash')
|
||||
warnings.filterwarnings("ignore", '.*unsupported hash')
|
||||
|
||||
# test string 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
|
||||
for row in chain(_known_hash_names, self.norm_hash_samples):
|
||||
for idx, format in enumerate(self.norm_hash_formats):
|
||||
correct = row[idx]
|
||||
for value in row:
|
||||
result = norm_hash_name(value, format)
|
||||
self.assertEqual(result, correct,
|
||||
"name=%r, format=%r:" % (value,
|
||||
format))
|
||||
|
||||
def test_lookup_hash_ctor(self):
|
||||
"""lookup_hash() -- constructor"""
|
||||
from passlib.crypto.digest import lookup_hash
|
||||
|
||||
# invalid/unknown names should be rejected
|
||||
self.assertRaises(ValueError, lookup_hash, "new")
|
||||
self.assertRaises(ValueError, lookup_hash, "__name__")
|
||||
self.assertRaises(ValueError, lookup_hash, "sha4")
|
||||
|
||||
# 1. should return hashlib builtin if found
|
||||
self.assertEqual(lookup_hash("md5"), (hashlib.md5, 16, 64))
|
||||
|
||||
# 2. should return wrapper around hashlib.new() if found
|
||||
try:
|
||||
hashlib.new("sha")
|
||||
has_sha = True
|
||||
except ValueError:
|
||||
has_sha = False
|
||||
if has_sha:
|
||||
record = lookup_hash("sha")
|
||||
const = record[0]
|
||||
self.assertEqual(record, (const, 20, 64))
|
||||
self.assertEqual(hexlify(const(b"abc").digest()),
|
||||
b"0164b8a914cd2a5e74c4f7ff082c4d97f1edf880")
|
||||
|
||||
else:
|
||||
self.assertRaises(ValueError, lookup_hash, "sha")
|
||||
|
||||
# 3. should fall back to builtin md4
|
||||
try:
|
||||
hashlib.new("md4")
|
||||
has_md4 = True
|
||||
except ValueError:
|
||||
has_md4 = False
|
||||
record = lookup_hash("md4")
|
||||
const = record[0]
|
||||
if not has_md4:
|
||||
from passlib.crypto._md4 import md4
|
||||
self.assertIs(const, md4)
|
||||
self.assertEqual(record, (const, 16, 64))
|
||||
self.assertEqual(hexlify(const(b"abc").digest()),
|
||||
b"a448017aaf21d8525fc10ae87aa6729d")
|
||||
|
||||
# should memoize records
|
||||
self.assertIs(lookup_hash("md5"), lookup_hash("md5"))
|
||||
|
||||
def test_lookup_hash_w_unknown_name(self):
|
||||
"""lookup_hash() -- unknown hash name"""
|
||||
from passlib.crypto.digest import lookup_hash
|
||||
|
||||
# unknown names should be rejected by default
|
||||
self.assertRaises(UnknownHashError, lookup_hash, "xxx256")
|
||||
|
||||
# required=False should return stub record instead
|
||||
info = lookup_hash("xxx256", required=False)
|
||||
self.assertFalse(info.supported)
|
||||
self.assertRaisesRegex(UnknownHashError, "unknown hash: 'xxx256'", info.const)
|
||||
self.assertEqual(info.name, "xxx256")
|
||||
self.assertEqual(info.digest_size, None)
|
||||
self.assertEqual(info.block_size, None)
|
||||
|
||||
# should cache stub records
|
||||
info2 = lookup_hash("xxx256", required=False)
|
||||
self.assertIs(info2, info)
|
||||
|
||||
def test_mock_fips_mode(self):
|
||||
"""
|
||||
lookup_hash() -- test set_mock_fips_mode()
|
||||
"""
|
||||
from passlib.crypto.digest import lookup_hash, _set_mock_fips_mode
|
||||
|
||||
# check if md5 is available so we can test mock helper
|
||||
if not lookup_hash("md5", required=False).supported:
|
||||
raise self.skipTest("md5 not supported")
|
||||
|
||||
# enable monkeypatch to mock up fips mode
|
||||
_set_mock_fips_mode()
|
||||
self.addCleanup(_set_mock_fips_mode, False)
|
||||
|
||||
pat = "'md5' hash disabled for fips"
|
||||
self.assertRaisesRegex(UnknownHashError, pat, lookup_hash, "md5")
|
||||
|
||||
info = lookup_hash("md5", required=False)
|
||||
self.assertRegex(info.error_text, pat)
|
||||
self.assertRaisesRegex(UnknownHashError, pat, info.const)
|
||||
|
||||
# should use hardcoded fallback info
|
||||
self.assertEqual(info.digest_size, 16)
|
||||
self.assertEqual(info.block_size, 64)
|
||||
|
||||
def test_lookup_hash_metadata(self):
|
||||
"""lookup_hash() -- metadata"""
|
||||
|
||||
from passlib.crypto.digest import lookup_hash
|
||||
|
||||
# quick test of metadata using known reference - sha256
|
||||
info = lookup_hash("sha256")
|
||||
self.assertEqual(info.name, "sha256")
|
||||
self.assertEqual(info.iana_name, "sha-256")
|
||||
self.assertEqual(info.block_size, 64)
|
||||
self.assertEqual(info.digest_size, 32)
|
||||
self.assertIs(lookup_hash("SHA2-256"), info)
|
||||
|
||||
# quick test of metadata using known reference - md5
|
||||
info = lookup_hash("md5")
|
||||
self.assertEqual(info.name, "md5")
|
||||
self.assertEqual(info.iana_name, "md5")
|
||||
self.assertEqual(info.block_size, 64)
|
||||
self.assertEqual(info.digest_size, 16)
|
||||
|
||||
def test_lookup_hash_alt_types(self):
|
||||
"""lookup_hash() -- alternate types"""
|
||||
|
||||
from passlib.crypto.digest import lookup_hash
|
||||
|
||||
info = lookup_hash("sha256")
|
||||
self.assertIs(lookup_hash(info), info)
|
||||
self.assertIs(lookup_hash(info.const), info)
|
||||
|
||||
self.assertRaises(TypeError, lookup_hash, 123)
|
||||
|
||||
# TODO: write full test of compile_hmac() -- currently relying on pbkdf2_hmac() tests
|
||||
|
||||
#=============================================================================
|
||||
# test PBKDF1 support
|
||||
#=============================================================================
|
||||
class Pbkdf1_Test(TestCase):
|
||||
"""test kdf helpers"""
|
||||
descriptionPrefix = "passlib.crypto.digest.pbkdf1"
|
||||
|
||||
pbkdf1_tests = [
|
||||
# (password, salt, rounds, keylen, hash, result)
|
||||
|
||||
#
|
||||
# from http://www.di-mgt.com.au/cryptoKDFs.html
|
||||
#
|
||||
(b'password', hb('78578E5A5D63CB06'), 1000, 16, 'sha1', hb('dc19847e05c64d2faf10ebfb4a3d2a20')),
|
||||
|
||||
#
|
||||
# custom
|
||||
#
|
||||
(b'password', b'salt', 1000, 0, 'md5', b''),
|
||||
(b'password', b'salt', 1000, 1, 'md5', hb('84')),
|
||||
(b'password', b'salt', 1000, 8, 'md5', hb('8475c6a8531a5d27')),
|
||||
(b'password', b'salt', 1000, 16, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
|
||||
(b'password', b'salt', 1000, None, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
|
||||
(b'password', b'salt', 1000, None, 'sha1', hb('4a8fd48e426ed081b535be5769892fa396293efb')),
|
||||
]
|
||||
if not JYTHON: # FIXME: find out why not jython, or reenable this.
|
||||
pbkdf1_tests.append(
|
||||
(b'password', b'salt', 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453'))
|
||||
)
|
||||
|
||||
def test_known(self):
|
||||
"""test reference vectors"""
|
||||
from passlib.crypto.digest import pbkdf1
|
||||
for secret, salt, rounds, keylen, digest, correct in self.pbkdf1_tests:
|
||||
result = pbkdf1(digest, secret, salt, rounds, keylen)
|
||||
self.assertEqual(result, correct)
|
||||
|
||||
def test_border(self):
|
||||
"""test border cases"""
|
||||
from passlib.crypto.digest import pbkdf1
|
||||
def helper(secret=b'secret', salt=b'salt', rounds=1, keylen=1, hash='md5'):
|
||||
return pbkdf1(hash, secret, salt, rounds, keylen)
|
||||
helper()
|
||||
|
||||
# salt/secret wrong type
|
||||
self.assertRaises(TypeError, helper, secret=1)
|
||||
self.assertRaises(TypeError, helper, salt=1)
|
||||
|
||||
# non-existent hashes
|
||||
self.assertRaises(ValueError, helper, hash='missing')
|
||||
|
||||
# rounds < 1 and wrong type
|
||||
self.assertRaises(ValueError, helper, rounds=0)
|
||||
self.assertRaises(TypeError, helper, rounds='1')
|
||||
|
||||
# keylen < 0, keylen > block_size, and wrong type
|
||||
self.assertRaises(ValueError, helper, keylen=-1)
|
||||
self.assertRaises(ValueError, helper, keylen=17, hash='md5')
|
||||
self.assertRaises(TypeError, helper, keylen='1')
|
||||
|
||||
#=============================================================================
|
||||
# test PBKDF2-HMAC support
|
||||
#=============================================================================
|
||||
|
||||
# import the test subject
|
||||
from passlib.crypto.digest import pbkdf2_hmac, PBKDF2_BACKENDS
|
||||
|
||||
# NOTE: relying on tox to verify this works under all the various backends.
|
||||
class Pbkdf2Test(TestCase):
|
||||
"""test pbkdf2() support"""
|
||||
descriptionPrefix = "passlib.crypto.digest.pbkdf2_hmac() <backends: %s>" % ", ".join(PBKDF2_BACKENDS)
|
||||
|
||||
pbkdf2_test_vectors = [
|
||||
# (result, secret, salt, rounds, keylen, digest="sha1")
|
||||
|
||||
#
|
||||
# from rfc 3962
|
||||
#
|
||||
|
||||
# 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
|
||||
),
|
||||
|
||||
#
|
||||
# from rfc 6070
|
||||
#
|
||||
(
|
||||
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
|
||||
##(
|
||||
##
|
||||
## hb("eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"),
|
||||
## "password", "salt", 16777216, 20,
|
||||
##),
|
||||
|
||||
(
|
||||
hb("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"),
|
||||
b"passwordPASSWORDpassword",
|
||||
b"saltSALTsaltSALTsaltSALTsaltSALTsalt",
|
||||
4096, 25,
|
||||
),
|
||||
|
||||
(
|
||||
hb("56fa6aa75548099dcc37d7f03425e0c3"),
|
||||
b"pass\00word", b"sa\00lt", 4096, 16,
|
||||
),
|
||||
|
||||
#
|
||||
# from example in http://grub.enbug.org/Authentication
|
||||
#
|
||||
(
|
||||
hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED"
|
||||
"97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC"
|
||||
"6C29E293F0A0"),
|
||||
b"hello",
|
||||
hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71"
|
||||
"784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073"
|
||||
"994D79080136"),
|
||||
10000, 64, "sha512"
|
||||
),
|
||||
|
||||
#
|
||||
# test vectors from fastpbkdf2 <https://github.com/ctz/fastpbkdf2/blob/master/testdata.py>
|
||||
#
|
||||
(
|
||||
hb('55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc'
|
||||
'49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783'),
|
||||
b'passwd', b'salt', 1, 64, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56'
|
||||
'a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d'),
|
||||
b'Password', b'NaCl', 80000, 64, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b'),
|
||||
b'password', b'salt', 1, 32, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43'),
|
||||
b'password', b'salt', 2, 32, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a'),
|
||||
b'password', b'salt', 4096, 32, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c'
|
||||
'635518c7dac47e9'),
|
||||
b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt',
|
||||
4096, 40, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('9e83f279c040f2a11aa4a02b24c418f2d3cb39560c9627fa4f47e3bcc2897c3d'),
|
||||
b'', b'salt', 1024, 32, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('ea5808411eb0c7e830deab55096cee582761e22a9bc034e3ece925225b07bf46'),
|
||||
b'password', b'', 1024, 32, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('89b69d0516f829893c696226650a8687'),
|
||||
b'pass\x00word', b'sa\x00lt', 4096, 16, 'sha256',
|
||||
),
|
||||
|
||||
(
|
||||
hb('867f70cf1ade02cff3752599a3a53dc4af34c7a669815ae5d513554e1c8cf252'),
|
||||
b'password', b'salt', 1, 32, 'sha512',
|
||||
),
|
||||
|
||||
(
|
||||
hb('e1d9c16aa681708a45f5c7c4e215ceb66e011a2e9f0040713f18aefdb866d53c'),
|
||||
b'password', b'salt', 2, 32, 'sha512',
|
||||
),
|
||||
|
||||
(
|
||||
hb('d197b1b33db0143e018b12f3d1d1479e6cdebdcc97c5c0f87f6902e072f457b5'),
|
||||
b'password', b'salt', 4096, 32, 'sha512',
|
||||
),
|
||||
|
||||
(
|
||||
hb('6e23f27638084b0f7ea1734e0d9841f55dd29ea60a834466f3396bac801fac1eeb'
|
||||
'63802f03a0b4acd7603e3699c8b74437be83ff01ad7f55dac1ef60f4d56480c35e'
|
||||
'e68fd52c6936'),
|
||||
b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt',
|
||||
1, 72, 'sha512',
|
||||
),
|
||||
|
||||
(
|
||||
hb('0c60c80f961f0e71f3a9b524af6012062fe037a6'),
|
||||
b'password', b'salt', 1, 20, 'sha1',
|
||||
),
|
||||
|
||||
#
|
||||
# custom tests
|
||||
#
|
||||
(
|
||||
hb('e248fb6b13365146f8ac6307cc222812'),
|
||||
b"secret", b"salt", 10, 16, "sha1",
|
||||
),
|
||||
(
|
||||
hb('e248fb6b13365146f8ac6307cc2228127872da6d'),
|
||||
b"secret", b"salt", 10, None, "sha1",
|
||||
),
|
||||
(
|
||||
hb('b1d5485772e6f76d5ebdc11b38d3eff0a5b2bd50dc11f937e86ecacd0cd40d1b'
|
||||
'9113e0734e3b76a3'),
|
||||
b"secret", b"salt", 62, 40, "md5",
|
||||
),
|
||||
(
|
||||
hb('ea014cc01f78d3883cac364bb5d054e2be238fb0b6081795a9d84512126e3129'
|
||||
'062104d2183464c4'),
|
||||
b"secret", b"salt", 62, 40, "md4",
|
||||
),
|
||||
]
|
||||
|
||||
def test_known(self):
|
||||
"""test reference vectors"""
|
||||
for row in self.pbkdf2_test_vectors:
|
||||
correct, secret, salt, rounds, keylen = row[:5]
|
||||
digest = row[5] if len(row) == 6 else "sha1"
|
||||
result = pbkdf2_hmac(digest, secret, salt, rounds, keylen)
|
||||
self.assertEqual(result, correct)
|
||||
|
||||
def test_backends(self):
|
||||
"""verify expected backends are present"""
|
||||
from passlib.crypto.digest import PBKDF2_BACKENDS
|
||||
|
||||
# check for fastpbkdf2
|
||||
try:
|
||||
import fastpbkdf2
|
||||
has_fastpbkdf2 = True
|
||||
except ImportError:
|
||||
has_fastpbkdf2 = False
|
||||
self.assertEqual("fastpbkdf2" in PBKDF2_BACKENDS, has_fastpbkdf2)
|
||||
|
||||
# check for hashlib
|
||||
try:
|
||||
from hashlib import pbkdf2_hmac
|
||||
has_hashlib_ssl = pbkdf2_hmac.__module__ != "hashlib"
|
||||
except ImportError:
|
||||
has_hashlib_ssl = False
|
||||
self.assertEqual("hashlib-ssl" in PBKDF2_BACKENDS, has_hashlib_ssl)
|
||||
|
||||
# check for appropriate builtin
|
||||
from passlib.utils.compat import PY3
|
||||
if PY3:
|
||||
self.assertIn("builtin-from-bytes", PBKDF2_BACKENDS)
|
||||
else:
|
||||
# XXX: only true as long as this is preferred over hexlify
|
||||
self.assertIn("builtin-unpack", PBKDF2_BACKENDS)
|
||||
|
||||
def test_border(self):
|
||||
"""test border cases"""
|
||||
def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, digest="sha1"):
|
||||
return pbkdf2_hmac(digest, secret, salt, rounds, keylen)
|
||||
helper()
|
||||
|
||||
# invalid rounds
|
||||
self.assertRaises(ValueError, helper, rounds=-1)
|
||||
self.assertRaises(ValueError, helper, rounds=0)
|
||||
self.assertRaises(TypeError, helper, rounds='x')
|
||||
|
||||
# invalid keylen
|
||||
helper(keylen=1)
|
||||
self.assertRaises(ValueError, helper, keylen=-1)
|
||||
self.assertRaises(ValueError, helper, keylen=0)
|
||||
# NOTE: hashlib actually throws error for keylen>=MAX_SINT32,
|
||||
# but pbkdf2 forbids anything > MAX_UINT32 * digest_size
|
||||
self.assertRaises(OverflowError, helper, keylen=20*(2**32-1)+1)
|
||||
self.assertRaises(TypeError, helper, keylen='x')
|
||||
|
||||
# invalid secret/salt type
|
||||
self.assertRaises(TypeError, helper, salt=5)
|
||||
self.assertRaises(TypeError, helper, secret=5)
|
||||
|
||||
# invalid hash
|
||||
self.assertRaises(ValueError, helper, digest='foo')
|
||||
self.assertRaises(TypeError, helper, digest=5)
|
||||
|
||||
def test_default_keylen(self):
|
||||
"""test keylen==None"""
|
||||
def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, digest="sha1"):
|
||||
return pbkdf2_hmac(digest, secret, salt, rounds, keylen)
|
||||
self.assertEqual(len(helper(digest='sha1')), 20)
|
||||
self.assertEqual(len(helper(digest='sha256')), 32)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -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
|
||||
#=============================================================================
|
||||
1080
venv/lib/python3.12/site-packages/passlib/tests/test_ext_django.py
Normal file
1080
venv/lib/python3.12/site-packages/passlib/tests/test_ext_django.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
#=============================================================================
|
||||
1819
venv/lib/python3.12/site-packages/passlib/tests/test_handlers.py
Normal file
1819
venv/lib/python3.12/site-packages/passlib/tests/test_handlers.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,688 @@
|
||||
"""passlib.tests.test_handlers - tests for passlib hash algorithms"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
import os
|
||||
import warnings
|
||||
# site
|
||||
# pkg
|
||||
from passlib import hash
|
||||
from passlib.handlers.bcrypt import IDENT_2, IDENT_2X
|
||||
from passlib.utils import repeat_string, to_bytes, is_safe_crypt_input
|
||||
from passlib.utils.compat import irange, PY3
|
||||
from passlib.tests.utils import HandlerCase, TEST_MODE
|
||||
from passlib.tests.test_handlers import UPASS_TABLE
|
||||
# module
|
||||
|
||||
#=============================================================================
|
||||
# bcrypt
|
||||
#=============================================================================
|
||||
class _bcrypt_test(HandlerCase):
|
||||
"""base for BCrypt test cases"""
|
||||
handler = hash.bcrypt
|
||||
reduce_default_rounds = True
|
||||
fuzz_salts_need_bcrypt_repair = True
|
||||
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# from JTR 1.7.9
|
||||
#
|
||||
('U*U*U*U*', '$2a$05$c92SVSfjeiCD6F2nAD6y0uBpJDjdRkt0EgeC4/31Rf2LUZbDRDE.O'),
|
||||
('U*U***U', '$2a$05$WY62Xk2TXZ7EvVDQ5fmjNu7b0GEzSzUXUh2cllxJwhtOeMtWV3Ujq'),
|
||||
('U*U***U*', '$2a$05$Fa0iKV3E2SYVUlMknirWU.CFYGvJ67UwVKI1E2FP6XeLiZGcH3MJi'),
|
||||
('*U*U*U*U', '$2a$05$.WRrXibc1zPgIdRXYfv.4uu6TD1KWf0VnHzq/0imhUhuxSxCyeBs2'),
|
||||
('', '$2a$05$Otz9agnajgrAe0.kFVF9V.tzaStZ2s1s4ZWi/LY4sw2k/MTVFj/IO'),
|
||||
|
||||
#
|
||||
# test vectors from http://www.openwall.com/crypt v1.2
|
||||
# note that this omits any hashes that depend on crypt_blowfish's
|
||||
# various CVE-2011-2483 workarounds (hash 2a and \xff\xff in password,
|
||||
# and any 2x hashes); and only contain hashes which are correct
|
||||
# under both crypt_blowfish 1.2 AND OpenBSD.
|
||||
#
|
||||
('U*U', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'),
|
||||
('U*U*', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.VGOzA784oUp/Z0DY336zx7pLYAy0lwK'),
|
||||
('U*U*U', '$2a$05$XXXXXXXXXXXXXXXXXXXXXOAcXxm9kjPGEMsLznoKqmqw7tc8WCx4a'),
|
||||
('', '$2a$05$CCCCCCCCCCCCCCCCCCCCC.7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy'),
|
||||
('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
'0123456789chars after 72 are ignored',
|
||||
'$2a$05$abcdefghijklmnopqrstuu5s2v8.iXieOjg/.AySBTTZIIVFJeBui'),
|
||||
(b'\xa3',
|
||||
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'),
|
||||
(b'\xff\xa3345',
|
||||
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.nRht2l/HRhr6zmCp9vYUvvsqynflf9e'),
|
||||
(b'\xa3ab',
|
||||
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.6IflQkJytoRVc1yuaNtHfiuq.FRlSIS'),
|
||||
(b'\xaa'*72 + b'chars after 72 are ignored as usual',
|
||||
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6'),
|
||||
(b'\xaa\x55'*36,
|
||||
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy'),
|
||||
(b'\x55\xaa\xff'*24,
|
||||
'$2a$05$/OK.fbVrR/bpIqNJ5ianF.9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe'),
|
||||
|
||||
# keeping one of their 2y tests, because we are supporting that.
|
||||
(b'\xa3',
|
||||
'$2y$05$/OK.fbVrR/bpIqNJ5ianF.Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq'),
|
||||
|
||||
#
|
||||
# 8bit bug (fixed in 2y/2b)
|
||||
#
|
||||
|
||||
# NOTE: see assert_lacks_8bit_bug() for origins of this test vector.
|
||||
(b"\xd1\x91", "$2y$05$6bNw2HLQYeqHYyBfLMsv/OUcZd0LKP39b87nBw3.S2tVZSqiQX6eu"),
|
||||
|
||||
#
|
||||
# bsd wraparound bug (fixed in 2b)
|
||||
#
|
||||
|
||||
# NOTE: if backend is vulnerable, password will hash the same as '0'*72
|
||||
# ("$2a$04$R1lJ2gkNaoPGdafE.H.16.nVyh2niHsGJhayOHLMiXlI45o8/DU.6"),
|
||||
# rather than same as ("0123456789"*8)[:72]
|
||||
# 255 should be sufficient, but checking
|
||||
(("0123456789"*26)[:254], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
|
||||
(("0123456789"*26)[:255], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
|
||||
(("0123456789"*26)[:256], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
|
||||
(("0123456789"*26)[:257], '$2a$04$R1lJ2gkNaoPGdafE.H.16.1MKHPvmKwryeulRe225LKProWYwt9Oi'),
|
||||
|
||||
|
||||
#
|
||||
# from py-bcrypt tests
|
||||
#
|
||||
('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
|
||||
('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'),
|
||||
('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'),
|
||||
('abcdefghijklmnopqrstuvwxyz',
|
||||
'$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
|
||||
('~!@#$%^&*() ~!@#$%^&*()PNBFRD',
|
||||
'$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'),
|
||||
|
||||
#
|
||||
# custom test vectors
|
||||
#
|
||||
|
||||
# ensures utf-8 used for unicode
|
||||
(UPASS_TABLE,
|
||||
'$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
|
||||
|
||||
# ensure 2b support
|
||||
(UPASS_TABLE,
|
||||
'$2b$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
|
||||
|
||||
]
|
||||
|
||||
if TEST_MODE("full"):
|
||||
#
|
||||
# add some extra tests related to 2/2a
|
||||
#
|
||||
CONFIG_2 = '$2$05$' + '.'*22
|
||||
CONFIG_A = '$2a$05$' + '.'*22
|
||||
known_correct_hashes.extend([
|
||||
("", CONFIG_2 + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
|
||||
("", CONFIG_A + 'J2ihDv8vVf7QZ9BsaRrKyqs2tkn55Yq'),
|
||||
("abc", CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
|
||||
("abc", CONFIG_A + 'ev6gDwpVye3oMCUpLY85aTpfBNHD0Ga'),
|
||||
("abc"*23, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
|
||||
("abc"*23, CONFIG_A + '2kIdfSj/4/R/Q6n847VTvc68BXiRYZC'),
|
||||
("abc"*24, CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
|
||||
("abc"*24, CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
|
||||
("abc"*24+'x', CONFIG_2 + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
|
||||
("abc"*24+'x', CONFIG_A + 'XuQjdH.wPVNUZ/bOfstdW/FqB8QSjte'),
|
||||
])
|
||||
|
||||
known_correct_configs = [
|
||||
('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE,
|
||||
'$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'),
|
||||
]
|
||||
|
||||
known_unidentified_hashes = [
|
||||
# invalid minor version
|
||||
"$2f$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
|
||||
"$2`$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
|
||||
]
|
||||
|
||||
known_malformed_hashes = [
|
||||
# bad char in otherwise correct hash
|
||||
# \/
|
||||
"$2a$12$EXRkfkdmXn!gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
|
||||
|
||||
# unsupported (but recognized) minor version
|
||||
"$2x$12$EXRkfkdmXnagzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q",
|
||||
|
||||
# rounds not zero-padded (py-bcrypt rejects this, therefore so do we)
|
||||
'$2a$6$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'
|
||||
|
||||
# NOTE: salts with padding bits set are technically malformed,
|
||||
# but we can reliably correct & issue a warning for that.
|
||||
]
|
||||
|
||||
platform_crypt_support = [
|
||||
("freedbsd|openbsd|netbsd", True),
|
||||
("darwin", False),
|
||||
("linux", None), # may be present via addon, e.g. debian's libpam-unix2
|
||||
("solaris", None), # depends on system policy
|
||||
]
|
||||
|
||||
#===================================================================
|
||||
# override some methods
|
||||
#===================================================================
|
||||
def setUp(self):
|
||||
# ensure builtin is enabled for duration of test.
|
||||
if TEST_MODE("full") and self.backend == "builtin":
|
||||
key = "PASSLIB_BUILTIN_BCRYPT"
|
||||
orig = os.environ.get(key)
|
||||
if orig:
|
||||
self.addCleanup(os.environ.__setitem__, key, orig)
|
||||
else:
|
||||
self.addCleanup(os.environ.__delitem__, key)
|
||||
os.environ[key] = "true"
|
||||
|
||||
super(_bcrypt_test, self).setUp()
|
||||
|
||||
# silence this warning, will come up a bunch during testing of old 2a hashes.
|
||||
warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*")
|
||||
|
||||
def populate_settings(self, kwds):
|
||||
# builtin is still just way too slow.
|
||||
if self.backend == "builtin":
|
||||
kwds.setdefault("rounds", 4)
|
||||
super(_bcrypt_test, self).populate_settings(kwds)
|
||||
|
||||
#===================================================================
|
||||
# fuzz testing
|
||||
#===================================================================
|
||||
def crypt_supports_variant(self, hash):
|
||||
"""check if OS crypt is expected to support given ident"""
|
||||
from passlib.handlers.bcrypt import bcrypt, IDENT_2X, IDENT_2Y
|
||||
from passlib.utils import safe_crypt
|
||||
ident = bcrypt.from_string(hash)
|
||||
return (safe_crypt("test", ident + "04$5BJqKfqMQvV7nS.yUguNcu") or "").startswith(ident)
|
||||
|
||||
fuzz_verifiers = HandlerCase.fuzz_verifiers + (
|
||||
"fuzz_verifier_bcrypt",
|
||||
"fuzz_verifier_pybcrypt",
|
||||
"fuzz_verifier_bcryptor",
|
||||
)
|
||||
|
||||
def fuzz_verifier_bcrypt(self):
|
||||
# test against bcrypt, if available
|
||||
from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y, _detect_pybcrypt
|
||||
from passlib.utils import to_native_str, to_bytes
|
||||
try:
|
||||
import bcrypt
|
||||
except ImportError:
|
||||
return
|
||||
if _detect_pybcrypt():
|
||||
return
|
||||
def check_bcrypt(secret, hash):
|
||||
"""bcrypt"""
|
||||
secret = to_bytes(secret, self.FuzzHashGenerator.password_encoding)
|
||||
if hash.startswith(IDENT_2B):
|
||||
# bcrypt <1.1 lacks 2B support
|
||||
hash = IDENT_2A + hash[4:]
|
||||
elif hash.startswith(IDENT_2):
|
||||
# bcrypt doesn't support $2$ hashes; but we can fake it
|
||||
# using the $2a$ algorithm, by repeating the password until
|
||||
# it's 72 chars in length.
|
||||
hash = IDENT_2A + hash[3:]
|
||||
if secret:
|
||||
secret = repeat_string(secret, 72)
|
||||
elif hash.startswith(IDENT_2Y) and bcrypt.__version__ == "3.0.0":
|
||||
hash = IDENT_2B + hash[4:]
|
||||
hash = to_bytes(hash)
|
||||
try:
|
||||
return bcrypt.hashpw(secret, hash) == hash
|
||||
except ValueError:
|
||||
raise ValueError("bcrypt rejected hash: %r (secret=%r)" % (hash, secret))
|
||||
return check_bcrypt
|
||||
|
||||
def fuzz_verifier_pybcrypt(self):
|
||||
# test against py-bcrypt, if available
|
||||
from passlib.handlers.bcrypt import (
|
||||
IDENT_2, IDENT_2A, IDENT_2B, IDENT_2X, IDENT_2Y,
|
||||
_PyBcryptBackend,
|
||||
)
|
||||
from passlib.utils import to_native_str
|
||||
|
||||
loaded = _PyBcryptBackend._load_backend_mixin("pybcrypt", False)
|
||||
if not loaded:
|
||||
return
|
||||
|
||||
from passlib.handlers.bcrypt import _pybcrypt as bcrypt_mod
|
||||
|
||||
lock = _PyBcryptBackend._calc_lock # reuse threadlock workaround for pybcrypt 0.2
|
||||
|
||||
def check_pybcrypt(secret, hash):
|
||||
"""pybcrypt"""
|
||||
secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding)
|
||||
if len(secret) > 200: # vulnerable to wraparound bug
|
||||
secret = secret[:200]
|
||||
if hash.startswith((IDENT_2B, IDENT_2Y)):
|
||||
hash = IDENT_2A + hash[4:]
|
||||
try:
|
||||
if lock:
|
||||
with lock:
|
||||
return bcrypt_mod.hashpw(secret, hash) == hash
|
||||
else:
|
||||
return bcrypt_mod.hashpw(secret, hash) == hash
|
||||
except ValueError:
|
||||
raise ValueError("py-bcrypt rejected hash: %r" % (hash,))
|
||||
return check_pybcrypt
|
||||
|
||||
def fuzz_verifier_bcryptor(self):
|
||||
# test against bcryptor if available
|
||||
from passlib.handlers.bcrypt import IDENT_2, IDENT_2A, IDENT_2Y, IDENT_2B
|
||||
from passlib.utils import to_native_str
|
||||
try:
|
||||
from bcryptor.engine import Engine
|
||||
except ImportError:
|
||||
return
|
||||
def check_bcryptor(secret, hash):
|
||||
"""bcryptor"""
|
||||
secret = to_native_str(secret, self.FuzzHashGenerator.password_encoding)
|
||||
if hash.startswith((IDENT_2B, IDENT_2Y)):
|
||||
hash = IDENT_2A + hash[4:]
|
||||
elif hash.startswith(IDENT_2):
|
||||
# bcryptor doesn't support $2$ hashes; but we can fake it
|
||||
# using the $2a$ algorithm, by repeating the password until
|
||||
# it's 72 chars in length.
|
||||
hash = IDENT_2A + hash[3:]
|
||||
if secret:
|
||||
secret = repeat_string(secret, 72)
|
||||
return Engine(False).hash_key(secret, hash) == hash
|
||||
return check_bcryptor
|
||||
|
||||
class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
|
||||
|
||||
def generate(self):
|
||||
opts = super(_bcrypt_test.FuzzHashGenerator, self).generate()
|
||||
|
||||
secret = opts['secret']
|
||||
other = opts['other']
|
||||
settings = opts['settings']
|
||||
ident = settings.get('ident')
|
||||
|
||||
if ident == IDENT_2X:
|
||||
# 2x is just recognized, not supported. don't test with it.
|
||||
del settings['ident']
|
||||
|
||||
elif ident == IDENT_2 and other and repeat_string(to_bytes(other), len(to_bytes(secret))) == to_bytes(secret):
|
||||
# avoid false failure due to flaw in 0-revision bcrypt:
|
||||
# repeated strings like 'abc' and 'abcabc' hash identically.
|
||||
opts['secret'], opts['other'] = self.random_password_pair()
|
||||
|
||||
return opts
|
||||
|
||||
def random_rounds(self):
|
||||
# decrease default rounds for fuzz testing to speed up volume.
|
||||
return self.randintgauss(5, 8, 6, 1)
|
||||
|
||||
#===================================================================
|
||||
# custom tests
|
||||
#===================================================================
|
||||
known_incorrect_padding = [
|
||||
# password, bad hash, good hash
|
||||
|
||||
# 2 bits of salt padding set
|
||||
# ("loppux", # \/
|
||||
# "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C",
|
||||
# "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"),
|
||||
("test", # \/
|
||||
'$2a$04$oaQbBqq8JnSM1NHRPQGXORY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO',
|
||||
'$2a$04$oaQbBqq8JnSM1NHRPQGXOOY4Vw3bdHKLIXTecPDRAcJ98cz1ilveO'),
|
||||
|
||||
# all 4 bits of salt padding set
|
||||
# ("Passlib11", # \/
|
||||
# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVcUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK",
|
||||
# "$2a$12$M8mKpW9a2vZ7PYhq/8eJVOUtKxpo6j0zAezu0G/HAMYgMkhPu4fLK"),
|
||||
("test", # \/
|
||||
"$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS",
|
||||
"$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"),
|
||||
|
||||
# bad checksum padding
|
||||
("test", # \/
|
||||
"$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIV",
|
||||
"$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"),
|
||||
]
|
||||
|
||||
def test_90_bcrypt_padding(self):
|
||||
"""test passlib correctly handles bcrypt padding bits"""
|
||||
self.require_TEST_MODE("full")
|
||||
#
|
||||
# prevents reccurrence of issue 25 (https://code.google.com/p/passlib/issues/detail?id=25)
|
||||
# were some unused bits were incorrectly set in bcrypt salt strings.
|
||||
# (fixed since 1.5.3)
|
||||
#
|
||||
bcrypt = self.handler
|
||||
corr_desc = ".*incorrectly set padding bits"
|
||||
|
||||
#
|
||||
# test hash() / genconfig() don't generate invalid salts anymore
|
||||
#
|
||||
def check_padding(hash):
|
||||
assert hash.startswith(("$2a$", "$2b$")) and len(hash) >= 28, \
|
||||
"unexpectedly malformed hash: %r" % (hash,)
|
||||
self.assertTrue(hash[28] in '.Oeu',
|
||||
"unused bits incorrectly set in hash: %r" % (hash,))
|
||||
for i in irange(6):
|
||||
check_padding(bcrypt.genconfig())
|
||||
for i in irange(3):
|
||||
check_padding(bcrypt.using(rounds=bcrypt.min_rounds).hash("bob"))
|
||||
|
||||
#
|
||||
# test genconfig() corrects invalid salts & issues warning.
|
||||
#
|
||||
with self.assertWarningList(["salt too large", corr_desc]):
|
||||
hash = bcrypt.genconfig(salt="."*21 + "A.", rounds=5, relaxed=True)
|
||||
self.assertEqual(hash, "$2b$05$" + "." * (22 + 31))
|
||||
|
||||
#
|
||||
# test public methods against good & bad hashes
|
||||
#
|
||||
samples = self.known_incorrect_padding
|
||||
for pwd, bad, good in samples:
|
||||
|
||||
# make sure genhash() corrects bad configs, leaves good unchanged
|
||||
with self.assertWarningList([corr_desc]):
|
||||
self.assertEqual(bcrypt.genhash(pwd, bad), good)
|
||||
with self.assertWarningList([]):
|
||||
self.assertEqual(bcrypt.genhash(pwd, good), good)
|
||||
|
||||
# make sure verify() works correctly with good & bad hashes
|
||||
with self.assertWarningList([corr_desc]):
|
||||
self.assertTrue(bcrypt.verify(pwd, bad))
|
||||
with self.assertWarningList([]):
|
||||
self.assertTrue(bcrypt.verify(pwd, good))
|
||||
|
||||
# make sure normhash() corrects bad hashes, leaves good unchanged
|
||||
with self.assertWarningList([corr_desc]):
|
||||
self.assertEqual(bcrypt.normhash(bad), good)
|
||||
with self.assertWarningList([]):
|
||||
self.assertEqual(bcrypt.normhash(good), good)
|
||||
|
||||
# make sure normhash() leaves non-bcrypt hashes alone
|
||||
self.assertEqual(bcrypt.normhash("$md5$abc"), "$md5$abc")
|
||||
|
||||
def test_needs_update_w_padding(self):
|
||||
"""needs_update corrects bcrypt padding"""
|
||||
# NOTE: see padding test above for details about issue this detects
|
||||
bcrypt = self.handler.using(rounds=4)
|
||||
|
||||
# PASS1 = "test"
|
||||
# bad contains invalid 'c' char at end of salt:
|
||||
# \/
|
||||
BAD1 = "$2a$04$yjDgE74RJkeqC0/1NheSScrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
|
||||
GOOD1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
|
||||
|
||||
self.assertTrue(bcrypt.needs_update(BAD1))
|
||||
self.assertFalse(bcrypt.needs_update(GOOD1))
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
# create test cases for specific backends
|
||||
bcrypt_bcrypt_test = _bcrypt_test.create_backend_case("bcrypt")
|
||||
bcrypt_pybcrypt_test = _bcrypt_test.create_backend_case("pybcrypt")
|
||||
bcrypt_bcryptor_test = _bcrypt_test.create_backend_case("bcryptor")
|
||||
|
||||
class bcrypt_os_crypt_test(_bcrypt_test.create_backend_case("os_crypt")):
|
||||
|
||||
# os crypt doesn't support non-utf8 secret bytes
|
||||
known_correct_hashes = [row for row in _bcrypt_test.known_correct_hashes
|
||||
if is_safe_crypt_input(row[0])]
|
||||
|
||||
# os crypt backend doesn't currently implement a per-call fallback if it fails
|
||||
has_os_crypt_fallback = False
|
||||
|
||||
bcrypt_builtin_test = _bcrypt_test.create_backend_case("builtin")
|
||||
|
||||
#=============================================================================
|
||||
# bcrypt
|
||||
#=============================================================================
|
||||
class _bcrypt_sha256_test(HandlerCase):
|
||||
"base for BCrypt-SHA256 test cases"
|
||||
handler = hash.bcrypt_sha256
|
||||
reduce_default_rounds = True
|
||||
forbidden_characters = None
|
||||
fuzz_salts_need_bcrypt_repair = True
|
||||
|
||||
known_correct_hashes = [
|
||||
#-------------------------------------------------------------------
|
||||
# custom test vectors for old v1 format
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
# empty
|
||||
("",
|
||||
'$bcrypt-sha256$2a,5$E/e/2AOhqM5W/KJTFQzLce$F6dYSxOdAEoJZO2eoHUZWZljW/e0TXO'),
|
||||
|
||||
# ascii
|
||||
("password",
|
||||
'$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
|
||||
|
||||
# unicode / utf8
|
||||
(UPASS_TABLE,
|
||||
'$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
|
||||
(UPASS_TABLE.encode("utf-8"),
|
||||
'$bcrypt-sha256$2a,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
|
||||
|
||||
# ensure 2b support
|
||||
("password",
|
||||
'$bcrypt-sha256$2b,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
|
||||
(UPASS_TABLE,
|
||||
'$bcrypt-sha256$2b,5$.US1fQ4TQS.ZTz/uJ5Kyn.$QNdPDOTKKT5/sovNz1iWg26quOU4Pje'),
|
||||
|
||||
# test >72 chars is hashed correctly -- under bcrypt these hash the same.
|
||||
# NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
|
||||
(repeat_string("abc123", 72),
|
||||
'$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$r/hyEtqJ0teqPEmfTLoZ83ciAI1Q74.'),
|
||||
(repeat_string("abc123", 72) + "qwr",
|
||||
'$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$021KLEif6epjot5yoxk0m8I0929ohEa'),
|
||||
(repeat_string("abc123", 72) + "xyz",
|
||||
'$bcrypt-sha256$2b,5$X1g1nh3g0v4h6970O68cxe$7.1kgpHduMGEjvM3fX6e/QCvfn6OKja'),
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
# custom test vectors for v2 format
|
||||
# TODO: convert to v2 format
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
# empty
|
||||
("",
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$E/e/2AOhqM5W/KJTFQzLce$WFPIZKtDDTriqWwlmRFfHiOTeheAZWe'),
|
||||
|
||||
# ascii
|
||||
("password",
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'),
|
||||
|
||||
# unicode / utf8
|
||||
(UPASS_TABLE,
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'),
|
||||
(UPASS_TABLE.encode("utf-8"),
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$.US1fQ4TQS.ZTz/uJ5Kyn.$pzzgp40k8reM1CuQb03PvE0IDPQSdV6'),
|
||||
|
||||
# test >72 chars is hashed correctly -- under bcrypt these hash the same.
|
||||
# NOTE: test_60_truncate_size() handles this already, this is just for overkill :)
|
||||
(repeat_string("abc123", 72),
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zu1cloESVFIOsUIo7fCEgkdHaI9SSue'),
|
||||
(repeat_string("abc123", 72) + "qwr",
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$CBF9csfEdW68xv3DwE6xSULXMtqEFP.'),
|
||||
(repeat_string("abc123", 72) + "xyz",
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$X1g1nh3g0v4h6970O68cxe$zC/1UDUG2ofEXB6Onr2vvyFzfhEOS3S'),
|
||||
]
|
||||
|
||||
known_correct_configs =[
|
||||
# v1
|
||||
('$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe',
|
||||
"password", '$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu'),
|
||||
# v2
|
||||
('$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe',
|
||||
"password", '$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$wOK1VFFtS8IGTrGa7.h5fs0u84qyPbS'),
|
||||
]
|
||||
|
||||
known_malformed_hashes = [
|
||||
#-------------------------------------------------------------------
|
||||
# v1 format
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
# bad char in otherwise correct hash
|
||||
# \/
|
||||
'$bcrypt-sha256$2a,5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# unrecognized bcrypt variant
|
||||
'$bcrypt-sha256$2c,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# unsupported bcrypt variant
|
||||
'$bcrypt-sha256$2x,5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# rounds zero-padded
|
||||
'$bcrypt-sha256$2a,05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# config string w/ $ added
|
||||
'$bcrypt-sha256$2a,5$5Hg1DKFqPE8C2aflZ5vVoe$',
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
# v2 format
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
# bad char in otherwise correct hash
|
||||
# \/
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKF!PE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# unsupported version (for this format)
|
||||
'$bcrypt-sha256$v=1,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# unrecognized version
|
||||
'$bcrypt-sha256$v=3,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# unrecognized bcrypt variant
|
||||
'$bcrypt-sha256$v=2,t=2c,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# unsupported bcrypt variant
|
||||
'$bcrypt-sha256$v=2,t=2a,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
'$bcrypt-sha256$v=2,t=2x,r=5$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# rounds zero-padded
|
||||
'$bcrypt-sha256$v=2,t=2b,r=05$5Hg1DKFqPE8C2aflZ5vVoe$12BjNE0p7axMg55.Y/mHsYiVuFBDQyu',
|
||||
|
||||
# config string w/ $ added
|
||||
'$bcrypt-sha256$v=2,t=2b,r=5$5Hg1DKFqPE8C2aflZ5vVoe$',
|
||||
]
|
||||
|
||||
#===================================================================
|
||||
# override some methods -- cloned from bcrypt
|
||||
#===================================================================
|
||||
def setUp(self):
|
||||
# ensure builtin is enabled for duration of test.
|
||||
if TEST_MODE("full") and self.backend == "builtin":
|
||||
key = "PASSLIB_BUILTIN_BCRYPT"
|
||||
orig = os.environ.get(key)
|
||||
if orig:
|
||||
self.addCleanup(os.environ.__setitem__, key, orig)
|
||||
else:
|
||||
self.addCleanup(os.environ.__delitem__, key)
|
||||
os.environ[key] = "enabled"
|
||||
super(_bcrypt_sha256_test, self).setUp()
|
||||
warnings.filterwarnings("ignore", ".*backend is vulnerable to the bsd wraparound bug.*")
|
||||
|
||||
def populate_settings(self, kwds):
|
||||
# builtin is still just way too slow.
|
||||
if self.backend == "builtin":
|
||||
kwds.setdefault("rounds", 4)
|
||||
super(_bcrypt_sha256_test, self).populate_settings(kwds)
|
||||
|
||||
#===================================================================
|
||||
# override ident tests for now
|
||||
#===================================================================
|
||||
|
||||
def require_many_idents(self):
|
||||
raise self.skipTest("multiple idents not supported")
|
||||
|
||||
def test_30_HasOneIdent(self):
|
||||
# forbidding ident keyword, we only support "2b" for now
|
||||
handler = self.handler
|
||||
handler(use_defaults=True)
|
||||
self.assertRaises(ValueError, handler, ident="$2y$", use_defaults=True)
|
||||
|
||||
#===================================================================
|
||||
# fuzz testing -- cloned from bcrypt
|
||||
#===================================================================
|
||||
|
||||
class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
|
||||
|
||||
def random_rounds(self):
|
||||
# decrease default rounds for fuzz testing to speed up volume.
|
||||
return self.randintgauss(5, 8, 6, 1)
|
||||
|
||||
def random_ident(self):
|
||||
return "2b"
|
||||
|
||||
#===================================================================
|
||||
# custom tests
|
||||
#===================================================================
|
||||
|
||||
def test_using_version(self):
|
||||
# default to v2
|
||||
handler = self.handler
|
||||
self.assertEqual(handler.version, 2)
|
||||
|
||||
# allow v1 explicitly
|
||||
subcls = handler.using(version=1)
|
||||
self.assertEqual(subcls.version, 1)
|
||||
|
||||
# forbid unknown ver
|
||||
self.assertRaises(ValueError, handler.using, version=999)
|
||||
|
||||
# allow '2a' only for v1
|
||||
subcls = handler.using(version=1, ident="2a")
|
||||
self.assertRaises(ValueError, handler.using, ident="2a")
|
||||
|
||||
def test_calc_digest_v2(self):
|
||||
"""
|
||||
test digest calc v2 matches bcrypt()
|
||||
"""
|
||||
from passlib.hash import bcrypt
|
||||
from passlib.crypto.digest import compile_hmac
|
||||
from passlib.utils.binary import b64encode
|
||||
|
||||
# manually calc intermediary digest
|
||||
salt = "nyKYxTAvjmy6lMDYMl11Uu"
|
||||
secret = "test"
|
||||
temp_digest = compile_hmac("sha256", salt.encode("ascii"))(secret.encode("ascii"))
|
||||
temp_digest = b64encode(temp_digest).decode("ascii")
|
||||
self.assertEqual(temp_digest, "J5TlyIDm+IcSWmKiDJm+MeICndBkFVPn4kKdJW8f+xY=")
|
||||
|
||||
# manually final hash from intermediary
|
||||
# XXX: genhash() could be useful here
|
||||
bcrypt_digest = bcrypt(ident="2b", salt=salt, rounds=12)._calc_checksum(temp_digest)
|
||||
self.assertEqual(bcrypt_digest, "M0wE0Ov/9LXoQFCe.jRHu3MSHPF54Ta")
|
||||
self.assertTrue(bcrypt.verify(temp_digest, "$2b$12$" + salt + bcrypt_digest))
|
||||
|
||||
# confirm handler outputs same thing.
|
||||
# XXX: genhash() could be useful here
|
||||
result = self.handler(ident="2b", salt=salt, rounds=12)._calc_checksum(secret)
|
||||
self.assertEqual(result, bcrypt_digest)
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
# create test cases for specific backends
|
||||
bcrypt_sha256_bcrypt_test = _bcrypt_sha256_test.create_backend_case("bcrypt")
|
||||
bcrypt_sha256_pybcrypt_test = _bcrypt_sha256_test.create_backend_case("pybcrypt")
|
||||
bcrypt_sha256_bcryptor_test = _bcrypt_sha256_test.create_backend_case("bcryptor")
|
||||
|
||||
class bcrypt_sha256_os_crypt_test(_bcrypt_sha256_test.create_backend_case("os_crypt")):
|
||||
|
||||
@classmethod
|
||||
def _get_safe_crypt_handler_backend(cls):
|
||||
return bcrypt_os_crypt_test._get_safe_crypt_handler_backend()
|
||||
|
||||
has_os_crypt_fallback = False
|
||||
|
||||
bcrypt_sha256_builtin_test = _bcrypt_sha256_test.create_backend_case("builtin")
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,457 @@
|
||||
"""
|
||||
passlib.tests.test_handlers_cisco - tests for Cisco-specific algorithms
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import absolute_import, division, print_function
|
||||
# core
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib import hash, exc
|
||||
from passlib.utils.compat import u
|
||||
from .utils import UserHandlerMixin, HandlerCase, repeat_string
|
||||
from .test_handlers import UPASS_TABLE
|
||||
# module
|
||||
__all__ = [
|
||||
"cisco_pix_test",
|
||||
"cisco_asa_test",
|
||||
"cisco_type7_test",
|
||||
]
|
||||
#=============================================================================
|
||||
# shared code for cisco PIX & ASA
|
||||
#=============================================================================
|
||||
|
||||
class _PixAsaSharedTest(UserHandlerMixin, HandlerCase):
|
||||
"""
|
||||
class w/ shared info for PIX & ASA tests.
|
||||
"""
|
||||
__unittest_skip = True # for TestCase
|
||||
requires_user = False # for UserHandlerMixin
|
||||
|
||||
#: shared list of hashes which should be identical under pix & asa7
|
||||
#: (i.e. combined secret + user < 17 bytes)
|
||||
pix_asa_shared_hashes = [
|
||||
#
|
||||
# http://www.perlmonks.org/index.pl?node_id=797623
|
||||
#
|
||||
(("cisco", ""), "2KFQnbNIdI.2KYOU"), # confirmed ASA 9.6
|
||||
|
||||
#
|
||||
# http://www.hsc.fr/ressources/breves/pix_crack.html.en
|
||||
#
|
||||
(("hsc", ""), "YtT8/k6Np8F1yz2c"), # confirmed ASA 9.6
|
||||
|
||||
#
|
||||
# www.freerainbowtables.com/phpBB3/viewtopic.php?f=2&t=1441
|
||||
#
|
||||
(("", ""), "8Ry2YjIyt7RRXU24"), # confirmed ASA 9.6
|
||||
(("cisco", "john"), "hN7LzeyYjw12FSIU"),
|
||||
(("cisco", "jack"), "7DrfeZ7cyOj/PslD"),
|
||||
|
||||
#
|
||||
# http://comments.gmane.org/gmane.comp.security.openwall.john.user/2529
|
||||
#
|
||||
(("ripper", "alex"), "h3mJrcH0901pqX/m"),
|
||||
(("cisco", "cisco"), "3USUcOPFUiMCO4Jk"),
|
||||
(("cisco", "cisco1"), "3USUcOPFUiMCO4Jk"),
|
||||
(("CscFw-ITC!", "admcom"), "lZt7HSIXw3.QP7.R"),
|
||||
("cangetin", "TynyB./ftknE77QP"),
|
||||
(("cangetin", "rramsey"), "jgBZqYtsWfGcUKDi"),
|
||||
|
||||
#
|
||||
# http://openwall.info/wiki/john/sample-hashes
|
||||
#
|
||||
(("phonehome", "rharris"), "zyIIMSYjiPm0L7a6"),
|
||||
|
||||
#
|
||||
# http://www.openwall.com/lists/john-users/2010/08/08/3
|
||||
#
|
||||
(("cangetin", ""), "TynyB./ftknE77QP"),
|
||||
(("cangetin", "rramsey"), "jgBZqYtsWfGcUKDi"),
|
||||
|
||||
#
|
||||
# from JTR 1.7.9
|
||||
#
|
||||
("test1", "TRPEas6f/aa6JSPL"),
|
||||
("test2", "OMT6mXmAvGyzrCtp"),
|
||||
("test3", "gTC7RIy1XJzagmLm"),
|
||||
("test4", "oWC1WRwqlBlbpf/O"),
|
||||
("password", "NuLKvvWGg.x9HEKO"),
|
||||
("0123456789abcdef", ".7nfVBEIEu4KbF/1"),
|
||||
|
||||
#
|
||||
# http://www.cisco.com/en/US/docs/security/pix/pix50/configuration/guide/commands.html#wp5472
|
||||
#
|
||||
(("1234567890123456", ""), "feCkwUGktTCAgIbD"), # canonical source
|
||||
(("watag00s1am", ""), "jMorNbK0514fadBh"), # canonical source
|
||||
|
||||
#
|
||||
# custom
|
||||
#
|
||||
(("cisco1", "cisco1"), "jmINXNH6p1BxUppp"),
|
||||
|
||||
# ensures utf-8 used for unicode
|
||||
(UPASS_TABLE, 'CaiIvkLMu2TOHXGT'),
|
||||
|
||||
#
|
||||
# passlib reference vectors
|
||||
#
|
||||
# Some of these have been confirmed on various ASA firewalls,
|
||||
# and the exact version is noted next to each hash.
|
||||
# Would like to verify these under more PIX & ASA versions.
|
||||
#
|
||||
# Those without a note are generally an extrapolation,
|
||||
# to ensure the code stays consistent, but for various reasons,
|
||||
# hasn't been verified.
|
||||
#
|
||||
# * One such case is usernames w/ 1 & 2 digits --
|
||||
# ASA (9.6 at least) requires 3+ digits in username.
|
||||
#
|
||||
# The following hashes (below 13 chars) should be identical for PIX/ASA.
|
||||
# Ones which differ are listed separately in the known_correct_hashes
|
||||
# list for the two test classes.
|
||||
#
|
||||
|
||||
# 4 char password
|
||||
(('1234', ''), 'RLPMUQ26KL4blgFN'), # confirmed ASA 9.6
|
||||
|
||||
# 8 char password
|
||||
(('01234567', ''), '0T52THgnYdV1tlOF'), # confirmed ASA 9.6
|
||||
(('01234567', '3'), '.z0dT9Alkdc7EIGS'),
|
||||
(('01234567', '36'), 'CC3Lam53t/mHhoE7'),
|
||||
(('01234567', '365'), '8xPrWpNnBdD2DzdZ'), # confirmed ASA 9.6
|
||||
(('01234567', '3333'), '.z0dT9Alkdc7EIGS'), # confirmed ASA 9.6
|
||||
(('01234567', '3636'), 'CC3Lam53t/mHhoE7'), # confirmed ASA 9.6
|
||||
(('01234567', '3653'), '8xPrWpNnBdD2DzdZ'), # confirmed ASA 9.6
|
||||
(('01234567', 'adm'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6
|
||||
(('01234567', 'adma'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6
|
||||
(('01234567', 'admad'), 'dfWs2qiao6KD/P2L'), # confirmed ASA 9.6
|
||||
(('01234567', 'user'), 'PNZ4ycbbZ0jp1.j1'), # confirmed ASA 9.6
|
||||
(('01234567', 'user1234'), 'PNZ4ycbbZ0jp1.j1'), # confirmed ASA 9.6
|
||||
|
||||
# 12 char password
|
||||
(('0123456789ab', ''), 'S31BxZOGlAigndcJ'), # confirmed ASA 9.6
|
||||
(('0123456789ab', '36'), 'wFqSX91X5.YaRKsi'),
|
||||
(('0123456789ab', '365'), 'qjgo3kNgTVxExbno'), # confirmed ASA 9.6
|
||||
(('0123456789ab', '3333'), 'mcXPL/vIZcIxLUQs'), # confirmed ASA 9.6
|
||||
(('0123456789ab', '3636'), 'wFqSX91X5.YaRKsi'), # confirmed ASA 9.6
|
||||
(('0123456789ab', '3653'), 'qjgo3kNgTVxExbno'), # confirmed ASA 9.6
|
||||
(('0123456789ab', 'user'), 'f.T4BKdzdNkjxQl7'), # confirmed ASA 9.6
|
||||
(('0123456789ab', 'user1234'), 'f.T4BKdzdNkjxQl7'), # confirmed ASA 9.6
|
||||
|
||||
# NOTE: remaining reference vectors for 13+ char passwords
|
||||
# are split up between cisco_pix & cisco_asa tests.
|
||||
|
||||
# unicode passwords
|
||||
# ASA supposedly uses utf-8 encoding, but entering non-ascii
|
||||
# chars is error-prone, and while UTF-8 appears to be intended,
|
||||
# observed behaviors include:
|
||||
# * ssh cli stripping non-ascii chars entirely
|
||||
# * ASDM web iface double-encoding utf-8 strings
|
||||
((u("t\xe1ble").encode("utf-8"), 'user'), 'Og8fB4NyF0m5Ed9c'),
|
||||
((u("t\xe1ble").encode("utf-8").decode("latin-1").encode("utf-8"),
|
||||
'user'), 'cMvFC2XVBmK/68yB'), # confirmed ASA 9.6 when typed into ASDM
|
||||
]
|
||||
|
||||
def test_calc_digest_spoiler(self):
|
||||
"""
|
||||
_calc_checksum() -- spoil oversize passwords during verify
|
||||
|
||||
for details, see 'spoil_digest' flag instead that function.
|
||||
this helps cisco_pix/cisco_asa implement their policy of
|
||||
``.truncate_verify_reject=True``.
|
||||
"""
|
||||
def calc(secret, for_hash=False):
|
||||
return self.handler(use_defaults=for_hash)._calc_checksum(secret)
|
||||
|
||||
# short (non-truncated) password
|
||||
short_secret = repeat_string("1234", self.handler.truncate_size)
|
||||
short_hash = calc(short_secret)
|
||||
|
||||
# longer password should have totally different hash,
|
||||
# to prevent verify from matching (i.e. "spoiled").
|
||||
long_secret = short_secret + "X"
|
||||
long_hash = calc(long_secret)
|
||||
self.assertNotEqual(long_hash, short_hash)
|
||||
|
||||
# spoiled hash should depend on whole secret,
|
||||
# so that output isn't predictable
|
||||
alt_long_secret = short_secret + "Y"
|
||||
alt_long_hash = calc(alt_long_secret)
|
||||
self.assertNotEqual(alt_long_hash, short_hash)
|
||||
self.assertNotEqual(alt_long_hash, long_hash)
|
||||
|
||||
# for hash(), should throw error if password too large
|
||||
calc(short_secret, for_hash=True)
|
||||
self.assertRaises(exc.PasswordSizeError, calc, long_secret, for_hash=True)
|
||||
self.assertRaises(exc.PasswordSizeError, calc, alt_long_secret, for_hash=True)
|
||||
|
||||
#=============================================================================
|
||||
# cisco pix
|
||||
#=============================================================================
|
||||
class cisco_pix_test(_PixAsaSharedTest):
|
||||
handler = hash.cisco_pix
|
||||
|
||||
#: known correct pix hashes
|
||||
known_correct_hashes = _PixAsaSharedTest.pix_asa_shared_hashes + [
|
||||
#
|
||||
# passlib reference vectors (PIX-specific)
|
||||
#
|
||||
# NOTE: See 'pix_asa_shared_hashes' for general PIX+ASA vectors,
|
||||
# and general notes about the 'passlib reference vectors' test set.
|
||||
#
|
||||
# All of the following are PIX-specific, as ASA starts
|
||||
# to use a different padding size at 13 characters.
|
||||
#
|
||||
# TODO: these need confirming w/ an actual PIX system.
|
||||
#
|
||||
|
||||
# 13 char password
|
||||
(('0123456789abc', ''), 'eacOpB7vE7ZDukSF'),
|
||||
(('0123456789abc', '3'), 'ylJTd/qei66WZe3w'),
|
||||
(('0123456789abc', '36'), 'hDx8QRlUhwd6bU8N'),
|
||||
(('0123456789abc', '365'), 'vYOOtnkh1HXcMrM7'),
|
||||
(('0123456789abc', '3333'), 'ylJTd/qei66WZe3w'),
|
||||
(('0123456789abc', '3636'), 'hDx8QRlUhwd6bU8N'),
|
||||
(('0123456789abc', '3653'), 'vYOOtnkh1HXcMrM7'),
|
||||
(('0123456789abc', 'user'), 'f4/.SALxqDo59mfV'),
|
||||
(('0123456789abc', 'user1234'), 'f4/.SALxqDo59mfV'),
|
||||
|
||||
# 14 char password
|
||||
(('0123456789abcd', ''), '6r8888iMxEoPdLp4'),
|
||||
(('0123456789abcd', '3'), 'f5lvmqWYj9gJqkIH'),
|
||||
(('0123456789abcd', '36'), 'OJJ1Khg5HeAYBH1c'),
|
||||
(('0123456789abcd', '365'), 'OJJ1Khg5HeAYBH1c'),
|
||||
(('0123456789abcd', '3333'), 'f5lvmqWYj9gJqkIH'),
|
||||
(('0123456789abcd', '3636'), 'OJJ1Khg5HeAYBH1c'),
|
||||
(('0123456789abcd', '3653'), 'OJJ1Khg5HeAYBH1c'),
|
||||
(('0123456789abcd', 'adm'), 'DbPLCFIkHc2SiyDk'),
|
||||
(('0123456789abcd', 'adma'), 'DbPLCFIkHc2SiyDk'),
|
||||
(('0123456789abcd', 'user'), 'WfO2UiTapPkF/FSn'),
|
||||
(('0123456789abcd', 'user1234'), 'WfO2UiTapPkF/FSn'),
|
||||
|
||||
# 15 char password
|
||||
(('0123456789abcde', ''), 'al1e0XFIugTYLai3'),
|
||||
(('0123456789abcde', '3'), 'lYbwBu.f82OIApQB'),
|
||||
(('0123456789abcde', '36'), 'lYbwBu.f82OIApQB'),
|
||||
(('0123456789abcde', '365'), 'lYbwBu.f82OIApQB'),
|
||||
(('0123456789abcde', '3333'), 'lYbwBu.f82OIApQB'),
|
||||
(('0123456789abcde', '3636'), 'lYbwBu.f82OIApQB'),
|
||||
(('0123456789abcde', '3653'), 'lYbwBu.f82OIApQB'),
|
||||
(('0123456789abcde', 'adm'), 'KgKx1UQvdR/09i9u'),
|
||||
(('0123456789abcde', 'adma'), 'KgKx1UQvdR/09i9u'),
|
||||
(('0123456789abcde', 'user'), 'qLopkenJ4WBqxaZN'),
|
||||
(('0123456789abcde', 'user1234'), 'qLopkenJ4WBqxaZN'),
|
||||
|
||||
# 16 char password
|
||||
(('0123456789abcdef', ''), '.7nfVBEIEu4KbF/1'),
|
||||
(('0123456789abcdef', '36'), '.7nfVBEIEu4KbF/1'),
|
||||
(('0123456789abcdef', '365'), '.7nfVBEIEu4KbF/1'),
|
||||
(('0123456789abcdef', '3333'), '.7nfVBEIEu4KbF/1'),
|
||||
(('0123456789abcdef', '3636'), '.7nfVBEIEu4KbF/1'),
|
||||
(('0123456789abcdef', '3653'), '.7nfVBEIEu4KbF/1'),
|
||||
(('0123456789abcdef', 'user'), '.7nfVBEIEu4KbF/1'),
|
||||
(('0123456789abcdef', 'user1234'), '.7nfVBEIEu4KbF/1'),
|
||||
]
|
||||
|
||||
|
||||
#=============================================================================
|
||||
# cisco asa
|
||||
#=============================================================================
|
||||
class cisco_asa_test(_PixAsaSharedTest):
|
||||
handler = hash.cisco_asa
|
||||
|
||||
known_correct_hashes = _PixAsaSharedTest.pix_asa_shared_hashes + [
|
||||
#
|
||||
# passlib reference vectors (ASA-specific)
|
||||
#
|
||||
# NOTE: See 'pix_asa_shared_hashes' for general PIX+ASA vectors,
|
||||
# and general notes about the 'passlib reference vectors' test set.
|
||||
#
|
||||
|
||||
# 13 char password
|
||||
# NOTE: past this point, ASA pads to 32 bytes instead of 16
|
||||
# for all cases where user is set (secret + 4 bytes > 16),
|
||||
# but still uses 16 bytes for enable pwds (secret <= 16).
|
||||
# hashes w/ user WON'T match PIX, but "enable" passwords will.
|
||||
(('0123456789abc', ''), 'eacOpB7vE7ZDukSF'), # confirmed ASA 9.6
|
||||
(('0123456789abc', '36'), 'FRV9JG18UBEgX0.O'),
|
||||
(('0123456789abc', '365'), 'NIwkusG9hmmMy6ZQ'), # confirmed ASA 9.6
|
||||
(('0123456789abc', '3333'), 'NmrkP98nT7RAeKZz'), # confirmed ASA 9.6
|
||||
(('0123456789abc', '3636'), 'FRV9JG18UBEgX0.O'), # confirmed ASA 9.6
|
||||
(('0123456789abc', '3653'), 'NIwkusG9hmmMy6ZQ'), # confirmed ASA 9.6
|
||||
(('0123456789abc', 'user'), '8Q/FZeam5ai1A47p'), # confirmed ASA 9.6
|
||||
(('0123456789abc', 'user1234'), '8Q/FZeam5ai1A47p'), # confirmed ASA 9.6
|
||||
|
||||
# 14 char password
|
||||
(('0123456789abcd', ''), '6r8888iMxEoPdLp4'), # confirmed ASA 9.6
|
||||
(('0123456789abcd', '3'), 'yxGoujXKPduTVaYB'),
|
||||
(('0123456789abcd', '36'), 'W0jckhnhjnr/DiT/'),
|
||||
(('0123456789abcd', '365'), 'HuVOxfMQNahaoF8u'), # confirmed ASA 9.6
|
||||
(('0123456789abcd', '3333'), 'yxGoujXKPduTVaYB'), # confirmed ASA 9.6
|
||||
(('0123456789abcd', '3636'), 'W0jckhnhjnr/DiT/'), # confirmed ASA 9.6
|
||||
(('0123456789abcd', '3653'), 'HuVOxfMQNahaoF8u'), # confirmed ASA 9.6
|
||||
(('0123456789abcd', 'adm'), 'RtOmSeoCs4AUdZqZ'), # confirmed ASA 9.6
|
||||
(('0123456789abcd', 'adma'), 'RtOmSeoCs4AUdZqZ'), # confirmed ASA 9.6
|
||||
(('0123456789abcd', 'user'), 'rrucwrcM0h25pr.m'), # confirmed ASA 9.6
|
||||
(('0123456789abcd', 'user1234'), 'rrucwrcM0h25pr.m'), # confirmed ASA 9.6
|
||||
|
||||
# 15 char password
|
||||
(('0123456789abcde', ''), 'al1e0XFIugTYLai3'), # confirmed ASA 9.6
|
||||
(('0123456789abcde', '3'), 'nAZrQoHaL.fgrIqt'),
|
||||
(('0123456789abcde', '36'), '2GxIQ6ICE795587X'),
|
||||
(('0123456789abcde', '365'), 'QmDsGwCRBbtGEKqM'), # confirmed ASA 9.6
|
||||
(('0123456789abcde', '3333'), 'nAZrQoHaL.fgrIqt'), # confirmed ASA 9.6
|
||||
(('0123456789abcde', '3636'), '2GxIQ6ICE795587X'), # confirmed ASA 9.6
|
||||
(('0123456789abcde', '3653'), 'QmDsGwCRBbtGEKqM'), # confirmed ASA 9.6
|
||||
(('0123456789abcde', 'adm'), 'Aj2aP0d.nk62wl4m'), # confirmed ASA 9.6
|
||||
(('0123456789abcde', 'adma'), 'Aj2aP0d.nk62wl4m'), # confirmed ASA 9.6
|
||||
(('0123456789abcde', 'user'), 'etxiXfo.bINJcXI7'), # confirmed ASA 9.6
|
||||
(('0123456789abcde', 'user1234'), 'etxiXfo.bINJcXI7'), # confirmed ASA 9.6
|
||||
|
||||
# 16 char password
|
||||
(('0123456789abcdef', ''), '.7nfVBEIEu4KbF/1'), # confirmed ASA 9.6
|
||||
(('0123456789abcdef', '36'), 'GhI8.yFSC5lwoafg'),
|
||||
(('0123456789abcdef', '365'), 'KFBI6cNQauyY6h/G'), # confirmed ASA 9.6
|
||||
(('0123456789abcdef', '3333'), 'Ghdi1IlsswgYzzMH'), # confirmed ASA 9.6
|
||||
(('0123456789abcdef', '3636'), 'GhI8.yFSC5lwoafg'), # confirmed ASA 9.6
|
||||
(('0123456789abcdef', '3653'), 'KFBI6cNQauyY6h/G'), # confirmed ASA 9.6
|
||||
(('0123456789abcdef', 'user'), 'IneB.wc9sfRzLPoh'), # confirmed ASA 9.6
|
||||
(('0123456789abcdef', 'user1234'), 'IneB.wc9sfRzLPoh'), # confirmed ASA 9.6
|
||||
|
||||
# 17 char password
|
||||
# NOTE: past this point, ASA pads to 32 bytes instead of 16
|
||||
# for ALL cases, since secret > 16 bytes even for enable pwds;
|
||||
# and so none of these rest here should match PIX.
|
||||
(('0123456789abcdefq', ''), 'bKshl.EN.X3CVFRQ'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefq', '36'), 'JAeTXHs0n30svlaG'),
|
||||
(('0123456789abcdefq', '365'), '4fKSSUBHT1ChGqHp'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefq', '3333'), 'USEJbxI6.VY4ecBP'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefq', '3636'), 'JAeTXHs0n30svlaG'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefq', '3653'), '4fKSSUBHT1ChGqHp'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefq', 'user'), '/dwqyD7nGdwSrDwk'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefq', 'user1234'), '/dwqyD7nGdwSrDwk'), # confirmed ASA 9.6
|
||||
|
||||
# 27 char password
|
||||
(('0123456789abcdefqwertyuiopa', ''), '4wp19zS3OCe.2jt5'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopa', '36'), 'PjUoGqWBKPyV9qOe'),
|
||||
(('0123456789abcdefqwertyuiopa', '365'), 'bfCy6xFAe5O/gzvM'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopa', '3333'), 'rd/ZMuGTJFIb2BNG'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopa', '3636'), 'PjUoGqWBKPyV9qOe'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopa', '3653'), 'bfCy6xFAe5O/gzvM'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopa', 'user'), 'zynfWw3UtszxLMgL'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopa', 'user1234'), 'zynfWw3UtszxLMgL'), # confirmed ASA 9.6
|
||||
|
||||
# 28 char password
|
||||
# NOTE: past this point, ASA stops appending the username AT ALL,
|
||||
# even though there's still room for the first few chars.
|
||||
(('0123456789abcdefqwertyuiopas', ''), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopas', '36'), 'W6nbOddI0SutTK7m'),
|
||||
(('0123456789abcdefqwertyuiopas', '365'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopas', 'user'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopas', 'user1234'), 'W6nbOddI0SutTK7m'), # confirmed ASA 9.6
|
||||
|
||||
# 32 char password
|
||||
# NOTE: this is max size that ASA allows, and throws error for larger
|
||||
(('0123456789abcdefqwertyuiopasdfgh', ''), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopasdfgh', '36'), '5hPT/iC6DnoBxo6a'),
|
||||
(('0123456789abcdefqwertyuiopasdfgh', '365'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopasdfgh', 'user'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6
|
||||
(('0123456789abcdefqwertyuiopasdfgh', 'user1234'), '5hPT/iC6DnoBxo6a'), # confirmed ASA 9.6
|
||||
]
|
||||
|
||||
|
||||
#=============================================================================
|
||||
# cisco type 7
|
||||
#=============================================================================
|
||||
class cisco_type7_test(HandlerCase):
|
||||
handler = hash.cisco_type7
|
||||
salt_bits = 4
|
||||
salt_type = int
|
||||
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# http://mccltd.net/blog/?p=1034
|
||||
#
|
||||
("secure ", "04480E051A33490E"),
|
||||
|
||||
#
|
||||
# http://insecure.org/sploits/cisco.passwords.html
|
||||
#
|
||||
("Its time to go to lunch!",
|
||||
"153B1F1F443E22292D73212D5300194315591954465A0D0B59"),
|
||||
|
||||
#
|
||||
# http://blog.ioshints.info/2007/11/type-7-decryption-in-cisco-ios.html
|
||||
#
|
||||
("t35t:pa55w0rd", "08351F1B1D431516475E1B54382F"),
|
||||
|
||||
#
|
||||
# http://www.m00nie.com/2011/09/cisco-type-7-password-decryption-and-encryption-with-perl/
|
||||
#
|
||||
("hiImTesting:)", "020E0D7206320A325847071E5F5E"),
|
||||
|
||||
#
|
||||
# http://packetlife.net/forums/thread/54/
|
||||
#
|
||||
("cisco123", "060506324F41584B56"),
|
||||
("cisco123", "1511021F07257A767B"),
|
||||
|
||||
#
|
||||
# source ?
|
||||
#
|
||||
('Supe&8ZUbeRp4SS', "06351A3149085123301517391C501918"),
|
||||
|
||||
#
|
||||
# custom
|
||||
#
|
||||
|
||||
# ensures utf-8 used for unicode
|
||||
(UPASS_TABLE, '0958EDC8A9F495F6F8A5FD'),
|
||||
]
|
||||
|
||||
known_unidentified_hashes = [
|
||||
# salt with hex value
|
||||
"0A480E051A33490E",
|
||||
|
||||
# salt value > 52. this may in fact be valid, but we reject it for now
|
||||
# (see docs for more).
|
||||
'99400E4812',
|
||||
]
|
||||
|
||||
def test_90_decode(self):
|
||||
"""test cisco_type7.decode()"""
|
||||
from passlib.utils import to_unicode, to_bytes
|
||||
|
||||
handler = self.handler
|
||||
for secret, hash in self.known_correct_hashes:
|
||||
usecret = to_unicode(secret)
|
||||
bsecret = to_bytes(secret)
|
||||
self.assertEqual(handler.decode(hash), usecret)
|
||||
self.assertEqual(handler.decode(hash, None), bsecret)
|
||||
|
||||
self.assertRaises(UnicodeDecodeError, handler.decode,
|
||||
'0958EDC8A9F495F6F8A5FD', 'ascii')
|
||||
|
||||
def test_91_salt(self):
|
||||
"""test salt value border cases"""
|
||||
handler = self.handler
|
||||
self.assertRaises(TypeError, handler, salt=None)
|
||||
handler(salt=None, use_defaults=True)
|
||||
self.assertRaises(TypeError, handler, salt='abc')
|
||||
self.assertRaises(ValueError, handler, salt=-10)
|
||||
self.assertRaises(ValueError, handler, salt=100)
|
||||
|
||||
self.assertRaises(TypeError, handler.using, salt='abc')
|
||||
self.assertRaises(ValueError, handler.using, salt=-10)
|
||||
self.assertRaises(ValueError, handler.using, salt=100)
|
||||
with self.assertWarningList("salt/offset must be.*"):
|
||||
subcls = handler.using(salt=100, relaxed=True)
|
||||
self.assertEqual(subcls(use_defaults=True).salt, 52)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,413 @@
|
||||
"""passlib.tests.test_handlers_django - tests for passlib hash algorithms"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
import re
|
||||
import warnings
|
||||
# site
|
||||
# pkg
|
||||
from passlib import hash
|
||||
from passlib.utils import repeat_string
|
||||
from passlib.utils.compat import u
|
||||
from passlib.tests.utils import TestCase, HandlerCase, skipUnless, SkipTest
|
||||
from passlib.tests.test_handlers import UPASS_USD, UPASS_TABLE
|
||||
from passlib.tests.test_ext_django import DJANGO_VERSION, MIN_DJANGO_VERSION, \
|
||||
check_django_hasher_has_backend
|
||||
# module
|
||||
|
||||
#=============================================================================
|
||||
# django
|
||||
#=============================================================================
|
||||
|
||||
# standard string django uses
|
||||
UPASS_LETMEIN = u('l\xe8tmein')
|
||||
|
||||
def vstr(version):
|
||||
return ".".join(str(e) for e in version)
|
||||
|
||||
class _DjangoHelper(TestCase):
|
||||
"""
|
||||
mixin for HandlerCase subclasses that are testing a hasher
|
||||
which is also present in django.
|
||||
"""
|
||||
__unittest_skip = True
|
||||
|
||||
#: minimum django version where hash alg is present / that we support testing against
|
||||
min_django_version = MIN_DJANGO_VERSION
|
||||
|
||||
#: max django version where hash alg is present
|
||||
#: TODO: for a bunch of the tests below, this is just max version where
|
||||
#: settings.PASSWORD_HASHERS includes it by default -- could add helper to patch
|
||||
#: desired django hasher back in for duration of test.
|
||||
#: XXX: change this to "disabled_in_django_version" instead?
|
||||
max_django_version = None
|
||||
|
||||
def _require_django_support(self):
|
||||
# make sure min django version
|
||||
if DJANGO_VERSION < self.min_django_version:
|
||||
raise self.skipTest("Django >= %s not installed" % vstr(self.min_django_version))
|
||||
if self.max_django_version and DJANGO_VERSION > self.max_django_version:
|
||||
raise self.skipTest("Django <= %s not installed" % vstr(self.max_django_version))
|
||||
|
||||
# make sure django has a backend for specified hasher
|
||||
name = self.handler.django_name
|
||||
if not check_django_hasher_has_backend(name):
|
||||
raise self.skipTest('django hasher %r not available' % name)
|
||||
|
||||
return True
|
||||
|
||||
extra_fuzz_verifiers = HandlerCase.fuzz_verifiers + (
|
||||
"fuzz_verifier_django",
|
||||
)
|
||||
|
||||
def fuzz_verifier_django(self):
|
||||
try:
|
||||
self._require_django_support()
|
||||
except SkipTest:
|
||||
return None
|
||||
from django.contrib.auth.hashers import check_password
|
||||
|
||||
def verify_django(secret, hash):
|
||||
"""django/check_password"""
|
||||
if self.handler.name == "django_bcrypt" and hash.startswith("bcrypt$$2y$"):
|
||||
hash = hash.replace("$$2y$", "$$2a$")
|
||||
if isinstance(secret, bytes):
|
||||
secret = secret.decode("utf-8")
|
||||
return check_password(secret, hash)
|
||||
return verify_django
|
||||
|
||||
def test_90_django_reference(self):
|
||||
"""run known correct hashes through Django's check_password()"""
|
||||
self._require_django_support()
|
||||
# XXX: esp. when it's no longer supported by django,
|
||||
# should verify it's *NOT* recognized
|
||||
from django.contrib.auth.hashers import check_password
|
||||
assert self.known_correct_hashes
|
||||
for secret, hash in self.iter_known_hashes():
|
||||
self.assertTrue(check_password(secret, hash),
|
||||
"secret=%r hash=%r failed to verify" %
|
||||
(secret, hash))
|
||||
self.assertFalse(check_password('x' + secret, hash),
|
||||
"mangled secret=%r hash=%r incorrect verified" %
|
||||
(secret, hash))
|
||||
|
||||
def test_91_django_generation(self):
|
||||
"""test against output of Django's make_password()"""
|
||||
self._require_django_support()
|
||||
# XXX: esp. when it's no longer supported by django,
|
||||
# should verify it's *NOT* recognized
|
||||
from passlib.utils import tick
|
||||
from django.contrib.auth.hashers import make_password
|
||||
name = self.handler.django_name # set for all the django_* handlers
|
||||
end = tick() + self.max_fuzz_time/2
|
||||
generator = self.FuzzHashGenerator(self, self.getRandom())
|
||||
while tick() < end:
|
||||
secret, other = generator.random_password_pair()
|
||||
if not secret: # django rejects empty passwords.
|
||||
continue
|
||||
if isinstance(secret, bytes):
|
||||
secret = secret.decode("utf-8")
|
||||
hash = make_password(secret, hasher=name)
|
||||
self.assertTrue(self.do_identify(hash))
|
||||
self.assertTrue(self.do_verify(secret, hash))
|
||||
self.assertFalse(self.do_verify(other, hash))
|
||||
|
||||
class django_disabled_test(HandlerCase):
|
||||
"""test django_disabled"""
|
||||
handler = hash.django_disabled
|
||||
disabled_contains_salt = True
|
||||
|
||||
known_correct_hashes = [
|
||||
# *everything* should hash to "!", and nothing should verify
|
||||
("password", "!"),
|
||||
("", "!"),
|
||||
(UPASS_TABLE, "!"),
|
||||
]
|
||||
|
||||
known_alternate_hashes = [
|
||||
# django 1.6 appends random alpnum string
|
||||
("!9wa845vn7098ythaehasldkfj", "password", "!"),
|
||||
]
|
||||
|
||||
class django_des_crypt_test(HandlerCase, _DjangoHelper):
|
||||
"""test django_des_crypt"""
|
||||
handler = hash.django_des_crypt
|
||||
max_django_version = (1,9)
|
||||
|
||||
known_correct_hashes = [
|
||||
# ensures only first two digits of salt count.
|
||||
("password", 'crypt$c2$c2M87q...WWcU'),
|
||||
("password", 'crypt$c2e86$c2M87q...WWcU'),
|
||||
("passwordignoreme", 'crypt$c2.AZ$c2M87q...WWcU'),
|
||||
|
||||
# ensures utf-8 used for unicode
|
||||
(UPASS_USD, 'crypt$c2e86$c2hN1Bxd6ZiWs'),
|
||||
(UPASS_TABLE, 'crypt$0.aQs$0.wB.TT0Czvlo'),
|
||||
(u("hell\u00D6"), "crypt$sa$saykDgk3BPZ9E"),
|
||||
|
||||
# prevent regression of issue 22
|
||||
("foo", 'crypt$MNVY.9ajgdvDQ$MNVY.9ajgdvDQ'),
|
||||
]
|
||||
|
||||
known_alternate_hashes = [
|
||||
# ensure django 1.4 empty salt field is accepted;
|
||||
# but that salt field is re-filled (for django 1.0 compatibility)
|
||||
('crypt$$c2M87q...WWcU', "password", 'crypt$c2$c2M87q...WWcU'),
|
||||
]
|
||||
|
||||
known_unidentified_hashes = [
|
||||
'sha1$aa$bb',
|
||||
]
|
||||
|
||||
known_malformed_hashes = [
|
||||
# checksum too short
|
||||
'crypt$c2$c2M87q',
|
||||
|
||||
# salt must be >2
|
||||
'crypt$f$c2M87q...WWcU',
|
||||
|
||||
# make sure first 2 chars of salt & chk field agree.
|
||||
'crypt$ffe86$c2M87q...WWcU',
|
||||
]
|
||||
|
||||
class django_salted_md5_test(HandlerCase, _DjangoHelper):
|
||||
"""test django_salted_md5"""
|
||||
handler = hash.django_salted_md5
|
||||
max_django_version = (1,9)
|
||||
|
||||
known_correct_hashes = [
|
||||
# test extra large salt
|
||||
("password", 'md5$123abcdef$c8272612932975ee80e8a35995708e80'),
|
||||
|
||||
# test django 1.4 alphanumeric salt
|
||||
("test", 'md5$3OpqnFAHW5CT$54b29300675271049a1ebae07b395e20'),
|
||||
|
||||
# ensures utf-8 used for unicode
|
||||
(UPASS_USD, 'md5$c2e86$92105508419a81a6babfaecf876a2fa0'),
|
||||
(UPASS_TABLE, 'md5$d9eb8$01495b32852bffb27cf5d4394fe7a54c'),
|
||||
]
|
||||
|
||||
known_unidentified_hashes = [
|
||||
'sha1$aa$bb',
|
||||
]
|
||||
|
||||
known_malformed_hashes = [
|
||||
# checksum too short
|
||||
'md5$aa$bb',
|
||||
]
|
||||
|
||||
class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
|
||||
|
||||
def random_salt_size(self):
|
||||
# workaround for django14 regression --
|
||||
# 1.4 won't accept hashes with empty salt strings, unlike 1.3 and earlier.
|
||||
# looks to be fixed in a future release -- https://code.djangoproject.com/ticket/18144
|
||||
# for now, we avoid salt_size==0 under 1.4
|
||||
handler = self.handler
|
||||
default = handler.default_salt_size
|
||||
assert handler.min_salt_size == 0
|
||||
lower = 1
|
||||
upper = handler.max_salt_size or default*4
|
||||
return self.randintgauss(lower, upper, default, default*.5)
|
||||
|
||||
class django_salted_sha1_test(HandlerCase, _DjangoHelper):
|
||||
"""test django_salted_sha1"""
|
||||
handler = hash.django_salted_sha1
|
||||
max_django_version = (1,9)
|
||||
|
||||
known_correct_hashes = [
|
||||
# test extra large salt
|
||||
("password",'sha1$123abcdef$e4a1877b0e35c47329e7ed7e58014276168a37ba'),
|
||||
|
||||
# test django 1.4 alphanumeric salt
|
||||
("test", 'sha1$bcwHF9Hy8lxS$6b4cfa0651b43161c6f1471ce9523acf1f751ba3'),
|
||||
|
||||
# ensures utf-8 used for unicode
|
||||
(UPASS_USD, 'sha1$c2e86$0f75c5d7fbd100d587c127ef0b693cde611b4ada'),
|
||||
(UPASS_TABLE, 'sha1$6d853$ef13a4d8fb57aed0cb573fe9c82e28dc7fd372d4'),
|
||||
|
||||
# generic password
|
||||
("MyPassword", 'sha1$54123$893cf12e134c3c215f3a76bd50d13f92404a54d3'),
|
||||
]
|
||||
|
||||
known_unidentified_hashes = [
|
||||
'md5$aa$bb',
|
||||
]
|
||||
|
||||
known_malformed_hashes = [
|
||||
# checksum too short
|
||||
'sha1$c2e86$0f75',
|
||||
]
|
||||
|
||||
# reuse custom random_salt_size() helper...
|
||||
FuzzHashGenerator = django_salted_md5_test.FuzzHashGenerator
|
||||
|
||||
class django_pbkdf2_sha256_test(HandlerCase, _DjangoHelper):
|
||||
"""test django_pbkdf2_sha256"""
|
||||
handler = hash.django_pbkdf2_sha256
|
||||
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# custom - generated via django 1.4 hasher
|
||||
#
|
||||
('not a password',
|
||||
'pbkdf2_sha256$10000$kjVJaVz6qsnJ$5yPHw3rwJGECpUf70daLGhOrQ5+AMxIJdz1c3bqK1Rs='),
|
||||
(UPASS_TABLE,
|
||||
'pbkdf2_sha256$10000$bEwAfNrH1TlQ$OgYUblFNUX1B8GfMqaCYUK/iHyO0pa7STTDdaEJBuY0='),
|
||||
]
|
||||
|
||||
class django_pbkdf2_sha1_test(HandlerCase, _DjangoHelper):
|
||||
"""test django_pbkdf2_sha1"""
|
||||
handler = hash.django_pbkdf2_sha1
|
||||
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# custom - generated via django 1.4 hashers
|
||||
#
|
||||
('not a password',
|
||||
'pbkdf2_sha1$10000$wz5B6WkasRoF$atJmJ1o+XfJxKq1+Nu1f1i57Z5I='),
|
||||
(UPASS_TABLE,
|
||||
'pbkdf2_sha1$10000$KZKWwvqb8BfL$rw5pWsxJEU4JrZAQhHTCO+u0f5Y='),
|
||||
]
|
||||
|
||||
@skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available")
|
||||
class django_bcrypt_test(HandlerCase, _DjangoHelper):
|
||||
"""test django_bcrypt"""
|
||||
handler = hash.django_bcrypt
|
||||
# XXX: not sure when this wasn't in default list anymore. somewhere in [2.0 - 2.2]
|
||||
max_django_version = (2, 0)
|
||||
fuzz_salts_need_bcrypt_repair = True
|
||||
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# just copied and adapted a few test vectors from bcrypt (above),
|
||||
# since django_bcrypt is just a wrapper for the real bcrypt class.
|
||||
#
|
||||
('', 'bcrypt$$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
|
||||
('abcdefghijklmnopqrstuvwxyz',
|
||||
'bcrypt$$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
|
||||
(UPASS_TABLE,
|
||||
'bcrypt$$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
|
||||
]
|
||||
|
||||
# NOTE: the following have been cloned from _bcrypt_test()
|
||||
|
||||
def populate_settings(self, kwds):
|
||||
# speed up test w/ lower rounds
|
||||
kwds.setdefault("rounds", 4)
|
||||
super(django_bcrypt_test, self).populate_settings(kwds)
|
||||
|
||||
class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
|
||||
|
||||
def random_rounds(self):
|
||||
# decrease default rounds for fuzz testing to speed up volume.
|
||||
return self.randintgauss(5, 8, 6, 1)
|
||||
|
||||
def random_ident(self):
|
||||
# omit multi-ident tests, only $2a$ counts for this class
|
||||
# XXX: enable this to check 2a / 2b?
|
||||
return None
|
||||
|
||||
@skipUnless(hash.bcrypt.has_backend(), "no bcrypt backends available")
|
||||
class django_bcrypt_sha256_test(HandlerCase, _DjangoHelper):
|
||||
"""test django_bcrypt_sha256"""
|
||||
handler = hash.django_bcrypt_sha256
|
||||
forbidden_characters = None
|
||||
fuzz_salts_need_bcrypt_repair = True
|
||||
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# custom - generated via django 1.6 hasher
|
||||
#
|
||||
('',
|
||||
'bcrypt_sha256$$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu'),
|
||||
(UPASS_LETMEIN,
|
||||
'bcrypt_sha256$$2a$08$NDjSAIcas.EcoxCRiArvT.MkNiPYVhrsrnJsRkLueZOoV1bsQqlmC'),
|
||||
(UPASS_TABLE,
|
||||
'bcrypt_sha256$$2a$06$kCXUnRFQptGg491siDKNTu8RxjBGSjALHRuvhPYNFsa4Ea5d9M48u'),
|
||||
|
||||
# test >72 chars is hashed correctly -- under bcrypt these hash the same.
|
||||
(repeat_string("abc123",72),
|
||||
'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OySmyXA8FoY4PjGizjE1QSDfuL5MXNni'),
|
||||
(repeat_string("abc123",72)+"qwr",
|
||||
'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61Ocy0BEz1RK6xslSNi8PlaLX2pe7x/KQG'),
|
||||
(repeat_string("abc123",72)+"xyz",
|
||||
'bcrypt_sha256$$2a$06$Tg/oYyZTyAf.Nb3qSgN61OvY2zoRVUa2Pugv2ExVOUT2YmhvxUFUa'),
|
||||
]
|
||||
|
||||
known_malformed_hashers = [
|
||||
# data in django salt field
|
||||
'bcrypt_sha256$xyz$2a$06$/3OeRpbOf8/l6nPPRdZPp.nRiyYqPobEZGdNRBWihQhiFDh1ws1tu',
|
||||
]
|
||||
|
||||
# NOTE: the following have been cloned from _bcrypt_test()
|
||||
|
||||
def populate_settings(self, kwds):
|
||||
# speed up test w/ lower rounds
|
||||
kwds.setdefault("rounds", 4)
|
||||
super(django_bcrypt_sha256_test, self).populate_settings(kwds)
|
||||
|
||||
class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
|
||||
|
||||
def random_rounds(self):
|
||||
# decrease default rounds for fuzz testing to speed up volume.
|
||||
return self.randintgauss(5, 8, 6, 1)
|
||||
|
||||
def random_ident(self):
|
||||
# omit multi-ident tests, only $2a$ counts for this class
|
||||
# XXX: enable this to check 2a / 2b?
|
||||
return None
|
||||
|
||||
from passlib.tests.test_handlers_argon2 import _base_argon2_test
|
||||
|
||||
@skipUnless(hash.argon2.has_backend(), "no argon2 backends available")
|
||||
class django_argon2_test(HandlerCase, _DjangoHelper):
|
||||
"""test django_bcrypt"""
|
||||
handler = hash.django_argon2
|
||||
|
||||
# NOTE: most of this adapted from _base_argon2_test & argon2pure test
|
||||
|
||||
known_correct_hashes = [
|
||||
# sample test
|
||||
("password", 'argon2$argon2i$v=19$m=256,t=1,p=1$c29tZXNhbHQ$AJFIsNZTMKTAewB4+ETN1A'),
|
||||
|
||||
# sample w/ all parameters different
|
||||
("password", 'argon2$argon2i$v=19$m=380,t=2,p=2$c29tZXNhbHQ$SrssP8n7m/12VWPM8dvNrw'),
|
||||
|
||||
# generated from django 1.10.3
|
||||
(UPASS_LETMEIN, 'argon2$argon2i$v=19$m=512,t=2,p=2$V25jN1l4UUJZWkR1$MxpA1BD2Gh7+D79gaAw6sQ'),
|
||||
]
|
||||
|
||||
def setUpWarnings(self):
|
||||
super(django_argon2_test, self).setUpWarnings()
|
||||
warnings.filterwarnings("ignore", ".*Using argon2pure backend.*")
|
||||
|
||||
def do_stub_encrypt(self, handler=None, **settings):
|
||||
# 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.wrapped(use_defaults=True)
|
||||
self.checksum = self._stub_checksum
|
||||
assert self.checksum
|
||||
return handler._wrap_hash(self.to_string())
|
||||
|
||||
def test_03_legacy_hash_workflow(self):
|
||||
# override base method
|
||||
raise self.skipTest("legacy 1.6 workflow not supported")
|
||||
|
||||
class FuzzHashGenerator(_base_argon2_test.FuzzHashGenerator):
|
||||
|
||||
def random_type(self):
|
||||
# override default since django only uses type I (see note in class)
|
||||
return "I"
|
||||
|
||||
def random_rounds(self):
|
||||
# decrease default rounds for fuzz testing to speed up volume.
|
||||
return self.randintgauss(1, 3, 2, 1)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,480 @@
|
||||
"""passlib.tests.test_handlers - tests for passlib hash algorithms"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
import warnings
|
||||
# site
|
||||
# pkg
|
||||
from passlib import hash
|
||||
from passlib.utils.compat import u
|
||||
from passlib.tests.utils import TestCase, HandlerCase
|
||||
from passlib.tests.test_handlers import UPASS_WAV
|
||||
# module
|
||||
|
||||
#=============================================================================
|
||||
# ldap_pbkdf2_{digest}
|
||||
#=============================================================================
|
||||
# NOTE: since these are all wrappers for the pbkdf2_{digest} hasehs,
|
||||
# they don't extensive separate testing.
|
||||
|
||||
class ldap_pbkdf2_test(TestCase):
|
||||
|
||||
def test_wrappers(self):
|
||||
"""test ldap pbkdf2 wrappers"""
|
||||
|
||||
self.assertTrue(
|
||||
hash.ldap_pbkdf2_sha1.verify(
|
||||
"password",
|
||||
'{PBKDF2}1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI',
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
hash.ldap_pbkdf2_sha256.verify(
|
||||
"password",
|
||||
'{PBKDF2-SHA256}1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg'
|
||||
'.fJPeq1h/gXXY7acBp9/6c.tmQ'
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
hash.ldap_pbkdf2_sha512.verify(
|
||||
"password",
|
||||
'{PBKDF2-SHA512}1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1'
|
||||
'7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww'
|
||||
)
|
||||
)
|
||||
|
||||
#=============================================================================
|
||||
# pbkdf2 hashes
|
||||
#=============================================================================
|
||||
class atlassian_pbkdf2_sha1_test(HandlerCase):
|
||||
handler = hash.atlassian_pbkdf2_sha1
|
||||
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# generated using Jira
|
||||
#
|
||||
("admin", '{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/p'),
|
||||
(UPASS_WAV,
|
||||
"{PKCS5S2}cE9Yq6Am5tQGdHSHhky2XLeOnURwzaLBG2sur7FHKpvy2u0qDn6GcVGRjlmJoIUy"),
|
||||
]
|
||||
|
||||
known_malformed_hashes = [
|
||||
# bad char ---\/
|
||||
'{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy!0IPksHChwoTAVYFrhsgoq8/p'
|
||||
|
||||
# bad size, missing padding
|
||||
'{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/'
|
||||
|
||||
# bad size, with correct padding
|
||||
'{PKCS5S2}c4xaeTQM0lUieMS3V5voiexyX9XhqC2dBd5ecVy60IPksHChwoTAVYFrhsgoq8/='
|
||||
]
|
||||
|
||||
class pbkdf2_sha1_test(HandlerCase):
|
||||
handler = hash.pbkdf2_sha1
|
||||
known_correct_hashes = [
|
||||
("password", '$pbkdf2$1212$OB.dtnSEXZK8U5cgxU/GYQ$y5LKPOplRmok7CZp/aqVDVg8zGI'),
|
||||
(UPASS_WAV,
|
||||
'$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc'),
|
||||
]
|
||||
|
||||
known_malformed_hashes = [
|
||||
# zero padded rounds field
|
||||
'$pbkdf2$01212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc',
|
||||
|
||||
# empty rounds field
|
||||
'$pbkdf2$$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc',
|
||||
|
||||
# too many field
|
||||
'$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc$',
|
||||
]
|
||||
|
||||
class pbkdf2_sha256_test(HandlerCase):
|
||||
handler = hash.pbkdf2_sha256
|
||||
known_correct_hashes = [
|
||||
("password",
|
||||
'$pbkdf2-sha256$1212$4vjV83LKPjQzk31VI4E0Vw$hsYF68OiOUPdDZ1Fg.fJPeq1h/gXXY7acBp9/6c.tmQ'
|
||||
),
|
||||
(UPASS_WAV,
|
||||
'$pbkdf2-sha256$1212$3SABFJGDtyhrQMVt1uABPw$WyaUoqCLgvz97s523nF4iuOqZNbp5Nt8do/cuaa7AiI'
|
||||
),
|
||||
]
|
||||
|
||||
class pbkdf2_sha512_test(HandlerCase):
|
||||
handler = hash.pbkdf2_sha512
|
||||
known_correct_hashes = [
|
||||
("password",
|
||||
'$pbkdf2-sha512$1212$RHY0Fr3IDMSVO/RSZyb5ow$eNLfBK.eVozomMr.1gYa1'
|
||||
'7k9B7KIK25NOEshvhrSX.esqY3s.FvWZViXz4KoLlQI.BzY/YTNJOiKc5gBYFYGww'
|
||||
),
|
||||
(UPASS_WAV,
|
||||
'$pbkdf2-sha512$1212$KkbvoKGsAIcF8IslDR6skQ$8be/PRmd88Ps8fmPowCJt'
|
||||
'tH9G3vgxpG.Krjt3KT.NP6cKJ0V4Prarqf.HBwz0dCkJ6xgWnSj2ynXSV7MlvMa8Q'
|
||||
),
|
||||
]
|
||||
|
||||
class cta_pbkdf2_sha1_test(HandlerCase):
|
||||
handler = hash.cta_pbkdf2_sha1
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# test vectors from original implementation
|
||||
#
|
||||
(u("hashy the \N{SNOWMAN}"), '$p5k2$1000$ZxK4ZBJCfQg=$jJZVscWtO--p1-xIZl6jhO2LKR0='),
|
||||
|
||||
#
|
||||
# custom
|
||||
#
|
||||
("password", "$p5k2$1$$h1TDLGSw9ST8UMAPeIE13i0t12c="),
|
||||
(UPASS_WAV,
|
||||
"$p5k2$4321$OTg3NjU0MzIx$jINJrSvZ3LXeIbUdrJkRpN62_WQ="),
|
||||
]
|
||||
|
||||
class dlitz_pbkdf2_sha1_test(HandlerCase):
|
||||
handler = hash.dlitz_pbkdf2_sha1
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# test vectors from original implementation
|
||||
#
|
||||
('cloadm', '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql'),
|
||||
('gnu', '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g'),
|
||||
('dcl', '$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL'),
|
||||
('spam', '$p5k2$3e8$H0NX9mT/$wk/sE8vv6OMKuMaqazCJYDSUhWY9YB2J'),
|
||||
(UPASS_WAV,
|
||||
'$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ'),
|
||||
]
|
||||
|
||||
class grub_pbkdf2_sha512_test(HandlerCase):
|
||||
handler = hash.grub_pbkdf2_sha512
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# test vectors generated from cmd line tool
|
||||
#
|
||||
|
||||
# salt=32 bytes
|
||||
(UPASS_WAV,
|
||||
'grub.pbkdf2.sha512.10000.BCAC1CEC5E4341C8C511C529'
|
||||
'7FA877BE91C2817B32A35A3ECF5CA6B8B257F751.6968526A'
|
||||
'2A5B1AEEE0A29A9E057336B48D388FFB3F600233237223C21'
|
||||
'04DE1752CEC35B0DD1ED49563398A282C0F471099C2803FBA'
|
||||
'47C7919CABC43192C68F60'),
|
||||
|
||||
# salt=64 bytes
|
||||
('toomanysecrets',
|
||||
'grub.pbkdf2.sha512.10000.9B436BB6978682363D5C449B'
|
||||
'BEAB322676946C632208BC1294D51F47174A9A3B04A7E4785'
|
||||
'986CD4EA7470FAB8FE9F6BD522D1FC6C51109A8596FB7AD48'
|
||||
'7C4493.0FE5EF169AFFCB67D86E2581B1E251D88C777B98BA'
|
||||
'2D3256ECC9F765D84956FC5CA5C4B6FD711AA285F0A04DCF4'
|
||||
'634083F9A20F4B6F339A52FBD6BED618E527B'),
|
||||
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# scram hash
|
||||
#=============================================================================
|
||||
class scram_test(HandlerCase):
|
||||
handler = hash.scram
|
||||
|
||||
# TODO: need a bunch more reference vectors from some real
|
||||
# SCRAM transactions.
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# taken from example in SCRAM specification (rfc 5802)
|
||||
#
|
||||
('pencil', '$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30'),
|
||||
|
||||
#
|
||||
# custom
|
||||
#
|
||||
|
||||
# same as 5802 example hash, but with sha-256 & sha-512 added.
|
||||
('pencil', '$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,'
|
||||
'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,'
|
||||
'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/'
|
||||
'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ'),
|
||||
|
||||
# test unicode passwords & saslprep (all the passwords below
|
||||
# should normalize to the same value: 'IX \xE0')
|
||||
(u('IX \xE0'), '$scram$6400$0BojBCBE6P2/N4bQ$'
|
||||
'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'),
|
||||
(u('\u2168\u3000a\u0300'), '$scram$6400$0BojBCBE6P2/N4bQ$'
|
||||
'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'),
|
||||
(u('\u00ADIX \xE0'), '$scram$6400$0BojBCBE6P2/N4bQ$'
|
||||
'sha-1=YniLes.b8WFMvBhtSACZyyvxeCc'),
|
||||
]
|
||||
|
||||
known_malformed_hashes = [
|
||||
# zero-padding in rounds
|
||||
'$scram$04096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30',
|
||||
|
||||
# non-digit in rounds
|
||||
'$scram$409A$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30',
|
||||
|
||||
# bad char in salt ---\/
|
||||
'$scram$4096$QSXCR.Q6sek8bf9-$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30',
|
||||
|
||||
# bad char in digest ---\/
|
||||
'$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX3-',
|
||||
|
||||
# missing sections
|
||||
'$scram$4096$QSXCR.Q6sek8bf92',
|
||||
'$scram$4096$QSXCR.Q6sek8bf92$',
|
||||
|
||||
# too many sections
|
||||
'$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30$',
|
||||
|
||||
# missing separator
|
||||
'$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30'
|
||||
'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY',
|
||||
|
||||
# too many chars in alg name
|
||||
'$scram$4096$QSXCR.Q6sek8bf92$sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,'
|
||||
'shaxxx-190=HZbuOlKbWl.eR8AfIposuKbhX30',
|
||||
|
||||
# missing sha-1 alg
|
||||
'$scram$4096$QSXCR.Q6sek8bf92$sha-256=HZbuOlKbWl.eR8AfIposuKbhX30',
|
||||
|
||||
# non-iana name
|
||||
'$scram$4096$QSXCR.Q6sek8bf92$sha1=HZbuOlKbWl.eR8AfIposuKbhX30',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(scram_test, self).setUp()
|
||||
|
||||
# some platforms lack stringprep (e.g. Jython, IronPython)
|
||||
self.require_stringprep()
|
||||
|
||||
# silence norm_hash_name() warning
|
||||
warnings.filterwarnings("ignore", r"norm_hash_name\(\): unknown hash")
|
||||
|
||||
def test_90_algs(self):
|
||||
"""test parsing of 'algs' setting"""
|
||||
defaults = dict(salt=b'A'*10, rounds=1000)
|
||||
def parse(algs, **kwds):
|
||||
for k in defaults:
|
||||
kwds.setdefault(k, defaults[k])
|
||||
return self.handler(algs=algs, **kwds).algs
|
||||
|
||||
# None -> default list
|
||||
self.assertEqual(parse(None, use_defaults=True), hash.scram.default_algs)
|
||||
self.assertRaises(TypeError, parse, None)
|
||||
|
||||
# strings should be parsed
|
||||
self.assertEqual(parse("sha1"), ["sha-1"])
|
||||
self.assertEqual(parse("sha1, sha256, md5"), ["md5","sha-1","sha-256"])
|
||||
|
||||
# lists should be normalized
|
||||
self.assertEqual(parse(["sha-1","sha256"]), ["sha-1","sha-256"])
|
||||
|
||||
# sha-1 required
|
||||
self.assertRaises(ValueError, parse, ["sha-256"])
|
||||
self.assertRaises(ValueError, parse, algs=[], use_defaults=True)
|
||||
|
||||
# alg names must be < 10 chars
|
||||
self.assertRaises(ValueError, parse, ["sha-1","shaxxx-190"])
|
||||
|
||||
# alg & checksum mutually exclusive.
|
||||
self.assertRaises(RuntimeError, parse, ['sha-1'],
|
||||
checksum={"sha-1": b"\x00"*20})
|
||||
|
||||
def test_90_checksums(self):
|
||||
"""test internal parsing of 'checksum' keyword"""
|
||||
# check non-bytes checksum values are rejected
|
||||
self.assertRaises(TypeError, self.handler, use_defaults=True,
|
||||
checksum={'sha-1': u('X')*20})
|
||||
|
||||
# check sha-1 is required
|
||||
self.assertRaises(ValueError, self.handler, use_defaults=True,
|
||||
checksum={'sha-256': b'X'*32})
|
||||
|
||||
# XXX: anything else that's not tested by the other code already?
|
||||
|
||||
def test_91_extract_digest_info(self):
|
||||
"""test scram.extract_digest_info()"""
|
||||
edi = self.handler.extract_digest_info
|
||||
|
||||
# return appropriate value or throw KeyError
|
||||
h = "$scram$10$AAAAAA$sha-1=AQ,bbb=Ag,ccc=Aw"
|
||||
s = b'\x00'*4
|
||||
self.assertEqual(edi(h,"SHA1"), (s,10, b'\x01'))
|
||||
self.assertEqual(edi(h,"bbb"), (s,10, b'\x02'))
|
||||
self.assertEqual(edi(h,"ccc"), (s,10, b'\x03'))
|
||||
self.assertRaises(KeyError, edi, h, "ddd")
|
||||
|
||||
# config strings should cause value error.
|
||||
c = "$scram$10$....$sha-1,bbb,ccc"
|
||||
self.assertRaises(ValueError, edi, c, "sha-1")
|
||||
self.assertRaises(ValueError, edi, c, "bbb")
|
||||
self.assertRaises(ValueError, edi, c, "ddd")
|
||||
|
||||
def test_92_extract_digest_algs(self):
|
||||
"""test scram.extract_digest_algs()"""
|
||||
eda = self.handler.extract_digest_algs
|
||||
|
||||
self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30'), ["sha-1"])
|
||||
|
||||
self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30', format="hashlib"),
|
||||
["sha1"])
|
||||
|
||||
self.assertEqual(eda('$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,'
|
||||
'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,'
|
||||
'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/'
|
||||
'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ'),
|
||||
["sha-1","sha-256","sha-512"])
|
||||
|
||||
def test_93_derive_digest(self):
|
||||
"""test scram.derive_digest()"""
|
||||
# NOTE: this just does a light test, since derive_digest
|
||||
# is used by hash / verify, and is tested pretty well via those.
|
||||
hash = self.handler.derive_digest
|
||||
|
||||
# check various encodings of password work.
|
||||
s1 = b'\x01\x02\x03'
|
||||
d1 = b'\xb2\xfb\xab\x82[tNuPnI\x8aZZ\x19\x87\xcen\xe9\xd3'
|
||||
self.assertEqual(hash(u("\u2168"), s1, 1000, 'sha-1'), d1)
|
||||
self.assertEqual(hash(b"\xe2\x85\xa8", s1, 1000, 'SHA-1'), d1)
|
||||
self.assertEqual(hash(u("IX"), s1, 1000, 'sha1'), d1)
|
||||
self.assertEqual(hash(b"IX", s1, 1000, 'SHA1'), d1)
|
||||
|
||||
# check algs
|
||||
self.assertEqual(hash("IX", s1, 1000, 'md5'),
|
||||
b'3\x19\x18\xc0\x1c/\xa8\xbf\xe4\xa3\xc2\x8eM\xe8od')
|
||||
self.assertRaises(ValueError, hash, "IX", s1, 1000, 'sha-666')
|
||||
|
||||
# check rounds
|
||||
self.assertRaises(ValueError, hash, "IX", s1, 0, 'sha-1')
|
||||
|
||||
# unicode salts accepted as of passlib 1.7 (previous caused TypeError)
|
||||
self.assertEqual(hash(u("IX"), s1.decode("latin-1"), 1000, 'sha1'), d1)
|
||||
|
||||
def test_94_saslprep(self):
|
||||
"""test hash/verify use saslprep"""
|
||||
# NOTE: this just does a light test that saslprep() is being
|
||||
# called in various places, relying in saslpreps()'s tests
|
||||
# to verify full normalization behavior.
|
||||
|
||||
# hash unnormalized
|
||||
h = self.do_encrypt(u("I\u00ADX"))
|
||||
self.assertTrue(self.do_verify(u("IX"), h))
|
||||
self.assertTrue(self.do_verify(u("\u2168"), h))
|
||||
|
||||
# hash normalized
|
||||
h = self.do_encrypt(u("\xF3"))
|
||||
self.assertTrue(self.do_verify(u("o\u0301"), h))
|
||||
self.assertTrue(self.do_verify(u("\u200Do\u0301"), h))
|
||||
|
||||
# throws error if forbidden char provided
|
||||
self.assertRaises(ValueError, self.do_encrypt, u("\uFDD0"))
|
||||
self.assertRaises(ValueError, self.do_verify, u("\uFDD0"), h)
|
||||
|
||||
def test_94_using_w_default_algs(self, param="default_algs"):
|
||||
"""using() -- 'default_algs' parameter"""
|
||||
# create subclass
|
||||
handler = self.handler
|
||||
orig = list(handler.default_algs) # in case it's modified in place
|
||||
subcls = handler.using(**{param: "sha1,md5"})
|
||||
|
||||
# shouldn't have changed handler
|
||||
self.assertEqual(handler.default_algs, orig)
|
||||
|
||||
# should have own set
|
||||
self.assertEqual(subcls.default_algs, ["md5", "sha-1"])
|
||||
|
||||
# test hash output
|
||||
h1 = subcls.hash("dummy")
|
||||
self.assertEqual(handler.extract_digest_algs(h1), ["md5", "sha-1"])
|
||||
|
||||
def test_94_using_w_algs(self):
|
||||
"""using() -- 'algs' parameter"""
|
||||
self.test_94_using_w_default_algs(param="algs")
|
||||
|
||||
def test_94_needs_update_algs(self):
|
||||
"""needs_update() -- algs setting"""
|
||||
handler1 = self.handler.using(algs="sha1,md5")
|
||||
|
||||
# shouldn't need update, has same algs
|
||||
h1 = handler1.hash("dummy")
|
||||
self.assertFalse(handler1.needs_update(h1))
|
||||
|
||||
# *currently* shouldn't need update, has superset of algs required by handler2
|
||||
# (may change this policy)
|
||||
handler2 = handler1.using(algs="sha1")
|
||||
self.assertFalse(handler2.needs_update(h1))
|
||||
|
||||
# should need update, doesn't have all algs required by handler3
|
||||
handler3 = handler1.using(algs="sha1,sha256")
|
||||
self.assertTrue(handler3.needs_update(h1))
|
||||
|
||||
def test_95_context_algs(self):
|
||||
"""test handling of 'algs' in context object"""
|
||||
handler = self.handler
|
||||
from passlib.context import CryptContext
|
||||
c1 = CryptContext(["scram"], scram__algs="sha1,md5")
|
||||
|
||||
h = c1.hash("dummy")
|
||||
self.assertEqual(handler.extract_digest_algs(h), ["md5", "sha-1"])
|
||||
self.assertFalse(c1.needs_update(h))
|
||||
|
||||
c2 = c1.copy(scram__algs="sha1")
|
||||
self.assertFalse(c2.needs_update(h))
|
||||
|
||||
c2 = c1.copy(scram__algs="sha1,sha256")
|
||||
self.assertTrue(c2.needs_update(h))
|
||||
|
||||
def test_96_full_verify(self):
|
||||
"""test verify(full=True) flag"""
|
||||
def vpart(s, h):
|
||||
return self.handler.verify(s, h)
|
||||
def vfull(s, h):
|
||||
return self.handler.verify(s, h, full=True)
|
||||
|
||||
# reference
|
||||
h = ('$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,'
|
||||
'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVY,'
|
||||
'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/'
|
||||
'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ')
|
||||
self.assertTrue(vfull('pencil', h))
|
||||
self.assertFalse(vfull('tape', h))
|
||||
|
||||
# catch truncated digests.
|
||||
h = ('$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,'
|
||||
'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhV,' # -1 char
|
||||
'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/'
|
||||
'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ')
|
||||
self.assertRaises(ValueError, vfull, 'pencil', h)
|
||||
|
||||
# catch padded digests.
|
||||
h = ('$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,'
|
||||
'sha-256=qXUXrlcvnaxxWG00DdRgVioR2gnUpuX5r.3EZ1rdhVYa,' # +1 char
|
||||
'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/'
|
||||
'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ')
|
||||
self.assertRaises(ValueError, vfull, 'pencil', h)
|
||||
|
||||
# catch hash containing digests belonging to diff passwords.
|
||||
# proper behavior for quick-verify (the default) is undefined,
|
||||
# but full-verify should throw error.
|
||||
h = ('$scram$4096$QSXCR.Q6sek8bf92$'
|
||||
'sha-1=HZbuOlKbWl.eR8AfIposuKbhX30,' # 'pencil'
|
||||
'sha-256=R7RJDWIbeKRTFwhE9oxh04kab0CllrQ3kCcpZUcligc,' # 'tape'
|
||||
'sha-512=lzgniLFcvglRLS0gt.C4gy.NurS3OIOVRAU1zZOV4P.qFiVFO2/' # 'pencil'
|
||||
'edGQSu/kD1LwdX0SNV/KsPdHSwEl5qRTuZQ')
|
||||
self.assertTrue(vpart('tape', h))
|
||||
self.assertFalse(vpart('pencil', h))
|
||||
self.assertRaises(ValueError, vfull, 'pencil', h)
|
||||
self.assertRaises(ValueError, vfull, 'tape', h)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,111 @@
|
||||
"""passlib.tests.test_handlers - tests for passlib hash algorithms"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*")
|
||||
# site
|
||||
# pkg
|
||||
from passlib import hash
|
||||
from passlib.tests.utils import HandlerCase, TEST_MODE
|
||||
from passlib.tests.test_handlers import UPASS_TABLE, PASS_TABLE_UTF8
|
||||
# module
|
||||
|
||||
#=============================================================================
|
||||
# scrypt hash
|
||||
#=============================================================================
|
||||
class _scrypt_test(HandlerCase):
|
||||
handler = hash.scrypt
|
||||
|
||||
known_correct_hashes = [
|
||||
#
|
||||
# excepted from test vectors from scrypt whitepaper
|
||||
# (http://www.tarsnap.com/scrypt/scrypt.pdf, appendix b),
|
||||
# and encoded using passlib's custom format
|
||||
#
|
||||
|
||||
# salt=b""
|
||||
("", "$scrypt$ln=4,r=1,p=1$$d9ZXYjhleyA7GcpCwYoEl/FrSETjB0ro39/6P+3iFEI"),
|
||||
|
||||
# salt=b"NaCl"
|
||||
("password", "$scrypt$ln=10,r=8,p=16$TmFDbA$/bq+HJ00cgB4VucZDQHp/nxq18vII3gw53N2Y0s3MWI"),
|
||||
|
||||
#
|
||||
# custom
|
||||
#
|
||||
|
||||
# simple test
|
||||
("test", '$scrypt$ln=8,r=8,p=1$wlhLyXmP8b53bm1NKYVQqg$mTpvG8lzuuDk+DWz8HZIB6Vum6erDuUm0As5yU+VxWA'),
|
||||
|
||||
# different block value
|
||||
("password", '$scrypt$ln=8,r=2,p=1$dO6d0xoDoLT2PofQGoNQag$g/Wf2A0vhHhaJM+addK61QPBthSmYB6uVTtQzh8CM3o'),
|
||||
|
||||
# different rounds
|
||||
(UPASS_TABLE, '$scrypt$ln=7,r=8,p=1$jjGmtDamdA4BQAjBeA9BSA$OiWRHhQtpDx7M/793x6UXK14AD512jg/qNm/hkWZG4M'),
|
||||
|
||||
# alt encoding
|
||||
(PASS_TABLE_UTF8, '$scrypt$ln=7,r=8,p=1$jjGmtDamdA4BQAjBeA9BSA$OiWRHhQtpDx7M/793x6UXK14AD512jg/qNm/hkWZG4M'),
|
||||
|
||||
# diff block & parallel counts as well
|
||||
("nacl", '$scrypt$ln=1,r=4,p=2$yhnD+J+Tci4lZCwFgHCuVQ$fAsEWmxSHuC0cHKMwKVFPzrQukgvK09Sj+NueTSxKds')
|
||||
]
|
||||
|
||||
if TEST_MODE("full"):
|
||||
# add some hashes with larger rounds value.
|
||||
known_correct_hashes.extend([
|
||||
#
|
||||
# from scrypt whitepaper
|
||||
#
|
||||
|
||||
# salt=b"SodiumChloride"
|
||||
("pleaseletmein", "$scrypt$ln=14,r=8,p=1$U29kaXVtQ2hsb3JpZGU"
|
||||
"$cCO9yzr9c0hGHAbNgf046/2o+7qQT44+qbVD9lRdofI"),
|
||||
|
||||
#
|
||||
# openwall format (https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt)
|
||||
#
|
||||
("pleaseletmein",
|
||||
"$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"),
|
||||
|
||||
])
|
||||
|
||||
known_malformed_hashes = [
|
||||
# missing 'p' value
|
||||
'$scrypt$ln=10,r=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ',
|
||||
|
||||
# rounds too low
|
||||
'$scrypt$ln=0,r=1,p=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ',
|
||||
|
||||
# invalid block size
|
||||
'$scrypt$ln=10,r=A,p=1$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ',
|
||||
|
||||
# r*p too large
|
||||
'$scrypt$ln=10,r=134217728,p=8$wvif8/4fg1Cq9V7L2dv73w$bJcLia1lyfQ1X2x0xflehwVXPzWIUQWWdnlGwfVzBeQ',
|
||||
]
|
||||
|
||||
def setUpWarnings(self):
|
||||
super(_scrypt_test, self).setUpWarnings()
|
||||
warnings.filterwarnings("ignore", ".*using builtin scrypt backend.*")
|
||||
|
||||
def populate_settings(self, kwds):
|
||||
# builtin is still just way too slow.
|
||||
if self.backend == "builtin":
|
||||
kwds.setdefault("rounds", 6)
|
||||
super(_scrypt_test, self).populate_settings(kwds)
|
||||
|
||||
class FuzzHashGenerator(HandlerCase.FuzzHashGenerator):
|
||||
|
||||
def random_rounds(self):
|
||||
# decrease default rounds for fuzz testing to speed up volume.
|
||||
return self.randintgauss(4, 10, 6, 1)
|
||||
|
||||
# create test cases for specific backends
|
||||
scrypt_stdlib_test = _scrypt_test.create_backend_case("stdlib")
|
||||
scrypt_scrypt_test = _scrypt_test.create_backend_case("scrypt")
|
||||
scrypt_builtin_test = _scrypt_test.create_backend_case("builtin")
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,97 @@
|
||||
"""test passlib.hosts"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib import hosts, hash as hashmod
|
||||
from passlib.utils import unix_crypt_schemes
|
||||
from passlib.tests.utils import TestCase
|
||||
# module
|
||||
|
||||
#=============================================================================
|
||||
# test predefined app contexts
|
||||
#=============================================================================
|
||||
class HostsTest(TestCase):
|
||||
"""perform general tests to make sure contexts work"""
|
||||
# NOTE: these tests are not really comprehensive,
|
||||
# since they would do little but duplicate
|
||||
# the presets in apps.py
|
||||
#
|
||||
# they mainly try to ensure no typos
|
||||
# or dynamic behavior foul-ups.
|
||||
|
||||
def check_unix_disabled(self, ctx):
|
||||
for hash in [
|
||||
"",
|
||||
"!",
|
||||
"*",
|
||||
"!$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0",
|
||||
]:
|
||||
self.assertEqual(ctx.identify(hash), 'unix_disabled')
|
||||
self.assertFalse(ctx.verify('test', hash))
|
||||
|
||||
def test_linux_context(self):
|
||||
ctx = hosts.linux_context
|
||||
for hash in [
|
||||
('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6'
|
||||
'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751'),
|
||||
('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny'
|
||||
'xDGgMlDcOsfaI17'),
|
||||
'$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0',
|
||||
'kAJJz.Rwp0A/I',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
self.check_unix_disabled(ctx)
|
||||
|
||||
def test_bsd_contexts(self):
|
||||
for ctx in [
|
||||
hosts.freebsd_context,
|
||||
hosts.openbsd_context,
|
||||
hosts.netbsd_context,
|
||||
]:
|
||||
for hash in [
|
||||
'$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0',
|
||||
'kAJJz.Rwp0A/I',
|
||||
]:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
h1 = "$2a$04$yjDgE74RJkeqC0/1NheSSOrvKeu9IbKDpcQf/Ox3qsrRS/Kw42qIS"
|
||||
if hashmod.bcrypt.has_backend():
|
||||
self.assertTrue(ctx.verify("test", h1))
|
||||
else:
|
||||
self.assertEqual(ctx.identify(h1), "bcrypt")
|
||||
self.check_unix_disabled(ctx)
|
||||
|
||||
def test_host_context(self):
|
||||
ctx = getattr(hosts, "host_context", None)
|
||||
if not ctx:
|
||||
return self.skipTest("host_context not available on this platform")
|
||||
|
||||
# validate schemes is non-empty,
|
||||
# and contains unix_disabled + at least one real scheme
|
||||
schemes = list(ctx.schemes())
|
||||
self.assertTrue(schemes, "appears to be unix system, but no known schemes supported by crypt")
|
||||
self.assertTrue('unix_disabled' in schemes)
|
||||
schemes.remove("unix_disabled")
|
||||
self.assertTrue(schemes, "should have schemes beside fallback scheme")
|
||||
self.assertTrue(set(unix_crypt_schemes).issuperset(schemes))
|
||||
|
||||
# check for hash support
|
||||
self.check_unix_disabled(ctx)
|
||||
for scheme, hash in [
|
||||
("sha512_crypt", ('$6$rounds=41128$VoQLvDjkaZ6L6BIE$4pt.1Ll1XdDYduEwEYPCMOBiR6W6'
|
||||
'znsyUEoNlcVXpv2gKKIbQolgmTGe6uEEVJ7azUxuc8Tf7zV9SD2z7Ij751')),
|
||||
("sha256_crypt", ('$5$rounds=31817$iZGmlyBQ99JSB5n6$p4E.pdPBWx19OajgjLRiOW0itGny'
|
||||
'xDGgMlDcOsfaI17')),
|
||||
("md5_crypt", '$1$TXl/FX/U$BZge.lr.ux6ekjEjxmzwz0'),
|
||||
("des_crypt", 'kAJJz.Rwp0A/I'),
|
||||
]:
|
||||
if scheme in schemes:
|
||||
self.assertTrue(ctx.verify("test", hash))
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
205
venv/lib/python3.12/site-packages/passlib/tests/test_pwd.py
Normal file
205
venv/lib/python3.12/site-packages/passlib/tests/test_pwd.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""passlib.tests -- tests for passlib.pwd"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import itertools
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.tests.utils import TestCase
|
||||
# local
|
||||
__all__ = [
|
||||
"UtilsTest",
|
||||
"GenerateTest",
|
||||
"StrengthTest",
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
#
|
||||
#=============================================================================
|
||||
class UtilsTest(TestCase):
|
||||
"""test internal utilities"""
|
||||
descriptionPrefix = "passlib.pwd"
|
||||
|
||||
def test_self_info_rate(self):
|
||||
"""_self_info_rate()"""
|
||||
from passlib.pwd import _self_info_rate
|
||||
|
||||
self.assertEqual(_self_info_rate(""), 0)
|
||||
|
||||
self.assertEqual(_self_info_rate("a" * 8), 0)
|
||||
|
||||
self.assertEqual(_self_info_rate("ab"), 1)
|
||||
self.assertEqual(_self_info_rate("ab" * 8), 1)
|
||||
|
||||
self.assertEqual(_self_info_rate("abcd"), 2)
|
||||
self.assertEqual(_self_info_rate("abcd" * 8), 2)
|
||||
self.assertAlmostEqual(_self_info_rate("abcdaaaa"), 1.5488, places=4)
|
||||
|
||||
# def test_total_self_info(self):
|
||||
# """_total_self_info()"""
|
||||
# from passlib.pwd import _total_self_info
|
||||
#
|
||||
# self.assertEqual(_total_self_info(""), 0)
|
||||
#
|
||||
# self.assertEqual(_total_self_info("a" * 8), 0)
|
||||
#
|
||||
# self.assertEqual(_total_self_info("ab"), 2)
|
||||
# self.assertEqual(_total_self_info("ab" * 8), 16)
|
||||
#
|
||||
# self.assertEqual(_total_self_info("abcd"), 8)
|
||||
# self.assertEqual(_total_self_info("abcd" * 8), 64)
|
||||
# self.assertAlmostEqual(_total_self_info("abcdaaaa"), 12.3904, places=4)
|
||||
|
||||
#=============================================================================
|
||||
# word generation
|
||||
#=============================================================================
|
||||
|
||||
# import subject
|
||||
from passlib.pwd import genword, default_charsets
|
||||
ascii_62 = default_charsets['ascii_62']
|
||||
hex = default_charsets['hex']
|
||||
|
||||
class WordGeneratorTest(TestCase):
|
||||
"""test generation routines"""
|
||||
descriptionPrefix = "passlib.pwd.genword()"
|
||||
|
||||
def setUp(self):
|
||||
super(WordGeneratorTest, self).setUp()
|
||||
|
||||
# patch some RNG references so they're reproducible.
|
||||
from passlib.pwd import SequenceGenerator
|
||||
self.patchAttr(SequenceGenerator, "rng",
|
||||
self.getRandom("pwd generator"))
|
||||
|
||||
def assertResultContents(self, results, count, chars, unique=True):
|
||||
"""check result list matches expected count & charset"""
|
||||
self.assertEqual(len(results), count)
|
||||
if unique:
|
||||
if unique is True:
|
||||
unique = count
|
||||
self.assertEqual(len(set(results)), unique)
|
||||
self.assertEqual(set("".join(results)), set(chars))
|
||||
|
||||
def test_general(self):
|
||||
"""general behavior"""
|
||||
|
||||
# basic usage
|
||||
result = genword()
|
||||
self.assertEqual(len(result), 9)
|
||||
|
||||
# malformed keyword should have useful error.
|
||||
self.assertRaisesRegex(TypeError, "(?i)unexpected keyword.*badkwd", genword, badkwd=True)
|
||||
|
||||
def test_returns(self):
|
||||
"""'returns' keyword"""
|
||||
# returns=int option
|
||||
results = genword(returns=5000)
|
||||
self.assertResultContents(results, 5000, ascii_62)
|
||||
|
||||
# returns=iter option
|
||||
gen = genword(returns=iter)
|
||||
results = [next(gen) for _ in range(5000)]
|
||||
self.assertResultContents(results, 5000, ascii_62)
|
||||
|
||||
# invalid returns option
|
||||
self.assertRaises(TypeError, genword, returns='invalid-type')
|
||||
|
||||
def test_charset(self):
|
||||
"""'charset' & 'chars' options"""
|
||||
# charset option
|
||||
results = genword(charset="hex", returns=5000)
|
||||
self.assertResultContents(results, 5000, hex)
|
||||
|
||||
# chars option
|
||||
# there are 3**3=27 possible combinations
|
||||
results = genword(length=3, chars="abc", returns=5000)
|
||||
self.assertResultContents(results, 5000, "abc", unique=27)
|
||||
|
||||
# chars + charset
|
||||
self.assertRaises(TypeError, genword, chars='abc', charset='hex')
|
||||
|
||||
# TODO: test rng option
|
||||
|
||||
#=============================================================================
|
||||
# phrase generation
|
||||
#=============================================================================
|
||||
|
||||
# import subject
|
||||
from passlib.pwd import genphrase
|
||||
simple_words = ["alpha", "beta", "gamma"]
|
||||
|
||||
class PhraseGeneratorTest(TestCase):
|
||||
"""test generation routines"""
|
||||
descriptionPrefix = "passlib.pwd.genphrase()"
|
||||
|
||||
def assertResultContents(self, results, count, words, unique=True, sep=" "):
|
||||
"""check result list matches expected count & charset"""
|
||||
self.assertEqual(len(results), count)
|
||||
if unique:
|
||||
if unique is True:
|
||||
unique = count
|
||||
self.assertEqual(len(set(results)), unique)
|
||||
out = set(itertools.chain.from_iterable(elem.split(sep) for elem in results))
|
||||
self.assertEqual(out, set(words))
|
||||
|
||||
def test_general(self):
|
||||
"""general behavior"""
|
||||
|
||||
# basic usage
|
||||
result = genphrase()
|
||||
self.assertEqual(len(result.split(" ")), 4) # 48 / log(7776, 2) ~= 3.7 -> 4
|
||||
|
||||
# malformed keyword should have useful error.
|
||||
self.assertRaisesRegex(TypeError, "(?i)unexpected keyword.*badkwd", genphrase, badkwd=True)
|
||||
|
||||
def test_entropy(self):
|
||||
"""'length' & 'entropy' keywords"""
|
||||
|
||||
# custom entropy
|
||||
result = genphrase(entropy=70)
|
||||
self.assertEqual(len(result.split(" ")), 6) # 70 / log(7776, 2) ~= 5.4 -> 6
|
||||
|
||||
# custom length
|
||||
result = genphrase(length=3)
|
||||
self.assertEqual(len(result.split(" ")), 3)
|
||||
|
||||
# custom length < entropy
|
||||
result = genphrase(length=3, entropy=48)
|
||||
self.assertEqual(len(result.split(" ")), 4)
|
||||
|
||||
# custom length > entropy
|
||||
result = genphrase(length=4, entropy=12)
|
||||
self.assertEqual(len(result.split(" ")), 4)
|
||||
|
||||
def test_returns(self):
|
||||
"""'returns' keyword"""
|
||||
# returns=int option
|
||||
results = genphrase(returns=1000, words=simple_words)
|
||||
self.assertResultContents(results, 1000, simple_words)
|
||||
|
||||
# returns=iter option
|
||||
gen = genphrase(returns=iter, words=simple_words)
|
||||
results = [next(gen) for _ in range(1000)]
|
||||
self.assertResultContents(results, 1000, simple_words)
|
||||
|
||||
# invalid returns option
|
||||
self.assertRaises(TypeError, genphrase, returns='invalid-type')
|
||||
|
||||
def test_wordset(self):
|
||||
"""'wordset' & 'words' options"""
|
||||
# wordset option
|
||||
results = genphrase(words=simple_words, returns=5000)
|
||||
self.assertResultContents(results, 5000, simple_words)
|
||||
|
||||
# words option
|
||||
results = genphrase(length=3, words=simple_words, returns=5000)
|
||||
self.assertResultContents(results, 5000, simple_words, unique=3**3)
|
||||
|
||||
# words + wordset
|
||||
self.assertRaises(TypeError, genphrase, words=simple_words, wordset='bip39')
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
228
venv/lib/python3.12/site-packages/passlib/tests/test_registry.py
Normal file
228
venv/lib/python3.12/site-packages/passlib/tests/test_registry.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""tests for passlib.hash -- (c) Assurance Technologies 2003-2009"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
from logging import getLogger
|
||||
import warnings
|
||||
import sys
|
||||
# site
|
||||
# pkg
|
||||
from passlib import hash, registry, exc
|
||||
from passlib.registry import register_crypt_handler, register_crypt_handler_path, \
|
||||
get_crypt_handler, list_crypt_handlers, _unload_handler_name as unload_handler_name
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.tests.utils import TestCase
|
||||
# module
|
||||
log = getLogger(__name__)
|
||||
|
||||
#=============================================================================
|
||||
# dummy handlers
|
||||
#
|
||||
# NOTE: these are defined outside of test case
|
||||
# since they're used by test_register_crypt_handler_path(),
|
||||
# which needs them to be available as module globals.
|
||||
#=============================================================================
|
||||
class dummy_0(uh.StaticHandler):
|
||||
name = "dummy_0"
|
||||
|
||||
class alt_dummy_0(uh.StaticHandler):
|
||||
name = "dummy_0"
|
||||
|
||||
dummy_x = 1
|
||||
|
||||
#=============================================================================
|
||||
# test registry
|
||||
#=============================================================================
|
||||
class RegistryTest(TestCase):
|
||||
|
||||
descriptionPrefix = "passlib.registry"
|
||||
|
||||
def setUp(self):
|
||||
super(RegistryTest, self).setUp()
|
||||
|
||||
# backup registry state & restore it after test.
|
||||
locations = dict(registry._locations)
|
||||
handlers = dict(registry._handlers)
|
||||
def restore():
|
||||
registry._locations.clear()
|
||||
registry._locations.update(locations)
|
||||
registry._handlers.clear()
|
||||
registry._handlers.update(handlers)
|
||||
self.addCleanup(restore)
|
||||
|
||||
def test_hash_proxy(self):
|
||||
"""test passlib.hash proxy object"""
|
||||
# check dir works
|
||||
dir(hash)
|
||||
|
||||
# check repr works
|
||||
repr(hash)
|
||||
|
||||
# check non-existent attrs raise error
|
||||
self.assertRaises(AttributeError, getattr, hash, 'fooey')
|
||||
|
||||
# GAE tries to set __loader__,
|
||||
# make sure that doesn't call register_crypt_handler.
|
||||
old = getattr(hash, "__loader__", None)
|
||||
test = object()
|
||||
hash.__loader__ = test
|
||||
self.assertIs(hash.__loader__, test)
|
||||
if old is None:
|
||||
del hash.__loader__
|
||||
self.assertFalse(hasattr(hash, "__loader__"))
|
||||
else:
|
||||
hash.__loader__ = old
|
||||
self.assertIs(hash.__loader__, old)
|
||||
|
||||
# check storing attr calls register_crypt_handler
|
||||
class dummy_1(uh.StaticHandler):
|
||||
name = "dummy_1"
|
||||
hash.dummy_1 = dummy_1
|
||||
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
|
||||
|
||||
# check storing under wrong name results in error
|
||||
self.assertRaises(ValueError, setattr, hash, "dummy_1x", dummy_1)
|
||||
|
||||
def test_register_crypt_handler_path(self):
|
||||
"""test register_crypt_handler_path()"""
|
||||
# NOTE: this messes w/ internals of registry, shouldn't be used publically.
|
||||
paths = registry._locations
|
||||
|
||||
# check namespace is clear
|
||||
self.assertTrue('dummy_0' not in paths)
|
||||
self.assertFalse(hasattr(hash, 'dummy_0'))
|
||||
|
||||
# check invalid names are rejected
|
||||
self.assertRaises(ValueError, register_crypt_handler_path,
|
||||
"dummy_0", ".test_registry")
|
||||
self.assertRaises(ValueError, register_crypt_handler_path,
|
||||
"dummy_0", __name__ + ":dummy_0:xxx")
|
||||
self.assertRaises(ValueError, register_crypt_handler_path,
|
||||
"dummy_0", __name__ + ":dummy_0.xxx")
|
||||
|
||||
# try lazy load
|
||||
register_crypt_handler_path('dummy_0', __name__)
|
||||
self.assertTrue('dummy_0' in list_crypt_handlers())
|
||||
self.assertTrue('dummy_0' not in list_crypt_handlers(loaded_only=True))
|
||||
self.assertIs(hash.dummy_0, dummy_0)
|
||||
self.assertTrue('dummy_0' in list_crypt_handlers(loaded_only=True))
|
||||
unload_handler_name('dummy_0')
|
||||
|
||||
# try lazy load w/ alt
|
||||
register_crypt_handler_path('dummy_0', __name__ + ':alt_dummy_0')
|
||||
self.assertIs(hash.dummy_0, alt_dummy_0)
|
||||
unload_handler_name('dummy_0')
|
||||
|
||||
# check lazy load w/ wrong type fails
|
||||
register_crypt_handler_path('dummy_x', __name__)
|
||||
self.assertRaises(TypeError, get_crypt_handler, 'dummy_x')
|
||||
|
||||
# check lazy load w/ wrong name fails
|
||||
register_crypt_handler_path('alt_dummy_0', __name__)
|
||||
self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0")
|
||||
unload_handler_name("alt_dummy_0")
|
||||
|
||||
# TODO: check lazy load which calls register_crypt_handler (warning should be issued)
|
||||
sys.modules.pop("passlib.tests._test_bad_register", None)
|
||||
register_crypt_handler_path("dummy_bad", "passlib.tests._test_bad_register")
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", "xxxxxxxxxx", DeprecationWarning)
|
||||
h = get_crypt_handler("dummy_bad")
|
||||
from passlib.tests import _test_bad_register as tbr
|
||||
self.assertIs(h, tbr.alt_dummy_bad)
|
||||
|
||||
def test_register_crypt_handler(self):
|
||||
"""test register_crypt_handler()"""
|
||||
|
||||
self.assertRaises(TypeError, register_crypt_handler, {})
|
||||
|
||||
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name=None)))
|
||||
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="AB_CD")))
|
||||
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab-cd")))
|
||||
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="ab__cd")))
|
||||
self.assertRaises(ValueError, register_crypt_handler, type('x', (uh.StaticHandler,), dict(name="default")))
|
||||
|
||||
class dummy_1(uh.StaticHandler):
|
||||
name = "dummy_1"
|
||||
|
||||
class dummy_1b(uh.StaticHandler):
|
||||
name = "dummy_1"
|
||||
|
||||
self.assertTrue('dummy_1' not in list_crypt_handlers())
|
||||
|
||||
register_crypt_handler(dummy_1)
|
||||
register_crypt_handler(dummy_1)
|
||||
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
|
||||
|
||||
self.assertRaises(KeyError, register_crypt_handler, dummy_1b)
|
||||
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
|
||||
|
||||
register_crypt_handler(dummy_1b, force=True)
|
||||
self.assertIs(get_crypt_handler("dummy_1"), dummy_1b)
|
||||
|
||||
self.assertTrue('dummy_1' in list_crypt_handlers())
|
||||
|
||||
def test_get_crypt_handler(self):
|
||||
"""test get_crypt_handler()"""
|
||||
|
||||
class dummy_1(uh.StaticHandler):
|
||||
name = "dummy_1"
|
||||
|
||||
# without available handler
|
||||
self.assertRaises(KeyError, get_crypt_handler, "dummy_1")
|
||||
self.assertIs(get_crypt_handler("dummy_1", None), None)
|
||||
|
||||
# already loaded handler
|
||||
register_crypt_handler(dummy_1)
|
||||
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning)
|
||||
|
||||
# already loaded handler, using incorrect name
|
||||
self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1)
|
||||
|
||||
# lazy load of unloaded handler, using incorrect name
|
||||
register_crypt_handler_path('dummy_0', __name__)
|
||||
self.assertIs(get_crypt_handler("DUMMY-0"), dummy_0)
|
||||
|
||||
# check system & private names aren't returned
|
||||
from passlib import hash
|
||||
hash.__dict__["_fake"] = "dummy"
|
||||
for name in ["_fake", "__package__"]:
|
||||
self.assertRaises(KeyError, get_crypt_handler, name)
|
||||
self.assertIs(get_crypt_handler(name, None), None)
|
||||
|
||||
def test_list_crypt_handlers(self):
|
||||
"""test list_crypt_handlers()"""
|
||||
from passlib.registry import list_crypt_handlers
|
||||
|
||||
# check system & private names aren't returned
|
||||
hash.__dict__["_fake"] = "dummy"
|
||||
for name in list_crypt_handlers():
|
||||
self.assertFalse(name.startswith("_"), "%r: " % name)
|
||||
unload_handler_name("_fake")
|
||||
|
||||
def test_handlers(self):
|
||||
"""verify we have tests for all builtin handlers"""
|
||||
from passlib.registry import list_crypt_handlers
|
||||
from passlib.tests.test_handlers import get_handler_case, conditionally_available_hashes
|
||||
for name in list_crypt_handlers():
|
||||
# skip some wrappers that don't need independant testing
|
||||
if name.startswith("ldap_") and name[5:] in list_crypt_handlers():
|
||||
continue
|
||||
if name in ["roundup_plaintext"]:
|
||||
continue
|
||||
# check the remaining ones all have a handler
|
||||
try:
|
||||
self.assertTrue(get_handler_case(name))
|
||||
except exc.MissingBackendError:
|
||||
if name in conditionally_available_hashes: # expected to fail on some setups
|
||||
continue
|
||||
raise
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
1604
venv/lib/python3.12/site-packages/passlib/tests/test_totp.py
Normal file
1604
venv/lib/python3.12/site-packages/passlib/tests/test_totp.py
Normal file
File diff suppressed because it is too large
Load Diff
1171
venv/lib/python3.12/site-packages/passlib/tests/test_utils.py
Normal file
1171
venv/lib/python3.12/site-packages/passlib/tests/test_utils.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,870 @@
|
||||
"""tests for passlib.hash -- (c) Assurance Technologies 2003-2009"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
import re
|
||||
import hashlib
|
||||
from logging import getLogger
|
||||
import warnings
|
||||
# site
|
||||
# pkg
|
||||
from passlib.hash import ldap_md5, sha256_crypt
|
||||
from passlib.exc import MissingBackendError, PasslibHashWarning
|
||||
from passlib.utils.compat import str_to_uascii, \
|
||||
uascii_to_str, unicode
|
||||
import passlib.utils.handlers as uh
|
||||
from passlib.tests.utils import HandlerCase, TestCase
|
||||
from passlib.utils.compat import u
|
||||
# module
|
||||
log = getLogger(__name__)
|
||||
|
||||
#=============================================================================
|
||||
# utils
|
||||
#=============================================================================
|
||||
def _makelang(alphabet, size):
|
||||
"""generate all strings of given size using alphabet"""
|
||||
def helper(size):
|
||||
if size < 2:
|
||||
for char in alphabet:
|
||||
yield char
|
||||
else:
|
||||
for char in alphabet:
|
||||
for tail in helper(size-1):
|
||||
yield char+tail
|
||||
return set(helper(size))
|
||||
|
||||
#=============================================================================
|
||||
# test GenericHandler & associates mixin classes
|
||||
#=============================================================================
|
||||
class SkeletonTest(TestCase):
|
||||
"""test hash support classes"""
|
||||
|
||||
#===================================================================
|
||||
# StaticHandler
|
||||
#===================================================================
|
||||
def test_00_static_handler(self):
|
||||
"""test StaticHandler class"""
|
||||
|
||||
class d1(uh.StaticHandler):
|
||||
name = "d1"
|
||||
context_kwds = ("flag",)
|
||||
_hash_prefix = u("_")
|
||||
checksum_chars = u("ab")
|
||||
checksum_size = 1
|
||||
|
||||
def __init__(self, flag=False, **kwds):
|
||||
super(d1, self).__init__(**kwds)
|
||||
self.flag = flag
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
return u('b') if self.flag else u('a')
|
||||
|
||||
# check default identify method
|
||||
self.assertTrue(d1.identify(u('_a')))
|
||||
self.assertTrue(d1.identify(b'_a'))
|
||||
self.assertTrue(d1.identify(u('_b')))
|
||||
|
||||
self.assertFalse(d1.identify(u('_c')))
|
||||
self.assertFalse(d1.identify(b'_c'))
|
||||
self.assertFalse(d1.identify(u('a')))
|
||||
self.assertFalse(d1.identify(u('b')))
|
||||
self.assertFalse(d1.identify(u('c')))
|
||||
self.assertRaises(TypeError, d1.identify, None)
|
||||
self.assertRaises(TypeError, d1.identify, 1)
|
||||
|
||||
# check default genconfig method
|
||||
self.assertEqual(d1.genconfig(), d1.hash(""))
|
||||
|
||||
# check default verify method
|
||||
self.assertTrue(d1.verify('s', b'_a'))
|
||||
self.assertTrue(d1.verify('s',u('_a')))
|
||||
self.assertFalse(d1.verify('s', b'_b'))
|
||||
self.assertFalse(d1.verify('s',u('_b')))
|
||||
self.assertTrue(d1.verify('s', b'_b', flag=True))
|
||||
self.assertRaises(ValueError, d1.verify, 's', b'_c')
|
||||
self.assertRaises(ValueError, d1.verify, 's', u('_c'))
|
||||
|
||||
# check default hash method
|
||||
self.assertEqual(d1.hash('s'), '_a')
|
||||
self.assertEqual(d1.hash('s', flag=True), '_b')
|
||||
|
||||
def test_01_calc_checksum_hack(self):
|
||||
"""test StaticHandler legacy attr"""
|
||||
# release 1.5 StaticHandler required genhash(),
|
||||
# not _calc_checksum, be implemented. we have backward compat wrapper,
|
||||
# this tests that it works.
|
||||
|
||||
class d1(uh.StaticHandler):
|
||||
name = "d1"
|
||||
|
||||
@classmethod
|
||||
def identify(cls, hash):
|
||||
if not hash or len(hash) != 40:
|
||||
return False
|
||||
try:
|
||||
int(hash, 16)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def genhash(cls, secret, hash):
|
||||
if secret is None:
|
||||
raise TypeError("no secret provided")
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
# NOTE: have to support hash=None since this is test of legacy 1.5 api
|
||||
if hash is not None and not cls.identify(hash):
|
||||
raise ValueError("invalid hash")
|
||||
return hashlib.sha1(b"xyz" + secret).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def verify(cls, secret, hash):
|
||||
if hash is None:
|
||||
raise ValueError("no hash specified")
|
||||
return cls.genhash(secret, hash) == hash.lower()
|
||||
|
||||
# hash should issue api warnings, but everything else should be fine.
|
||||
with self.assertWarningList("d1.*should be updated.*_calc_checksum"):
|
||||
hash = d1.hash("test")
|
||||
self.assertEqual(hash, '7c622762588a0e5cc786ad0a143156f9fd38eea3')
|
||||
|
||||
self.assertTrue(d1.verify("test", hash))
|
||||
self.assertFalse(d1.verify("xtest", hash))
|
||||
|
||||
# not defining genhash either, however, should cause NotImplementedError
|
||||
del d1.genhash
|
||||
self.assertRaises(NotImplementedError, d1.hash, 'test')
|
||||
|
||||
#===================================================================
|
||||
# GenericHandler & mixins
|
||||
#===================================================================
|
||||
def test_10_identify(self):
|
||||
"""test GenericHandler.identify()"""
|
||||
class d1(uh.GenericHandler):
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
if isinstance(hash, bytes):
|
||||
hash = hash.decode("ascii")
|
||||
if hash == u('a'):
|
||||
return cls(checksum=hash)
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
# check fallback
|
||||
self.assertRaises(TypeError, d1.identify, None)
|
||||
self.assertRaises(TypeError, d1.identify, 1)
|
||||
self.assertFalse(d1.identify(''))
|
||||
self.assertTrue(d1.identify('a'))
|
||||
self.assertFalse(d1.identify('b'))
|
||||
|
||||
# check regexp
|
||||
d1._hash_regex = re.compile(u('@.'))
|
||||
self.assertRaises(TypeError, d1.identify, None)
|
||||
self.assertRaises(TypeError, d1.identify, 1)
|
||||
self.assertTrue(d1.identify('@a'))
|
||||
self.assertFalse(d1.identify('a'))
|
||||
del d1._hash_regex
|
||||
|
||||
# check ident-based
|
||||
d1.ident = u('!')
|
||||
self.assertRaises(TypeError, d1.identify, None)
|
||||
self.assertRaises(TypeError, d1.identify, 1)
|
||||
self.assertTrue(d1.identify('!a'))
|
||||
self.assertFalse(d1.identify('a'))
|
||||
del d1.ident
|
||||
|
||||
def test_11_norm_checksum(self):
|
||||
"""test GenericHandler checksum handling"""
|
||||
# setup helpers
|
||||
class d1(uh.GenericHandler):
|
||||
name = 'd1'
|
||||
checksum_size = 4
|
||||
checksum_chars = u('xz')
|
||||
|
||||
def norm_checksum(checksum=None, **k):
|
||||
return d1(checksum=checksum, **k).checksum
|
||||
|
||||
# too small
|
||||
self.assertRaises(ValueError, norm_checksum, u('xxx'))
|
||||
|
||||
# right size
|
||||
self.assertEqual(norm_checksum(u('xxxx')), u('xxxx'))
|
||||
self.assertEqual(norm_checksum(u('xzxz')), u('xzxz'))
|
||||
|
||||
# too large
|
||||
self.assertRaises(ValueError, norm_checksum, u('xxxxx'))
|
||||
|
||||
# wrong chars
|
||||
self.assertRaises(ValueError, norm_checksum, u('xxyx'))
|
||||
|
||||
# wrong type
|
||||
self.assertRaises(TypeError, norm_checksum, b'xxyx')
|
||||
|
||||
# relaxed
|
||||
# NOTE: this could be turned back on if we test _norm_checksum() directly...
|
||||
#with self.assertWarningList("checksum should be unicode"):
|
||||
# self.assertEqual(norm_checksum(b'xxzx', relaxed=True), u('xxzx'))
|
||||
#self.assertRaises(TypeError, norm_checksum, 1, relaxed=True)
|
||||
|
||||
# test _stub_checksum behavior
|
||||
self.assertEqual(d1()._stub_checksum, u('xxxx'))
|
||||
|
||||
def test_12_norm_checksum_raw(self):
|
||||
"""test GenericHandler + HasRawChecksum mixin"""
|
||||
class d1(uh.HasRawChecksum, uh.GenericHandler):
|
||||
name = 'd1'
|
||||
checksum_size = 4
|
||||
|
||||
def norm_checksum(*a, **k):
|
||||
return d1(*a, **k).checksum
|
||||
|
||||
# test bytes
|
||||
self.assertEqual(norm_checksum(b'1234'), b'1234')
|
||||
|
||||
# test unicode
|
||||
self.assertRaises(TypeError, norm_checksum, u('xxyx'))
|
||||
|
||||
# NOTE: this could be turned back on if we test _norm_checksum() directly...
|
||||
# self.assertRaises(TypeError, norm_checksum, u('xxyx'), relaxed=True)
|
||||
|
||||
# test _stub_checksum behavior
|
||||
self.assertEqual(d1()._stub_checksum, b'\x00'*4)
|
||||
|
||||
def test_20_norm_salt(self):
|
||||
"""test GenericHandler + HasSalt mixin"""
|
||||
# setup helpers
|
||||
class d1(uh.HasSalt, uh.GenericHandler):
|
||||
name = 'd1'
|
||||
setting_kwds = ('salt',)
|
||||
min_salt_size = 2
|
||||
max_salt_size = 4
|
||||
default_salt_size = 3
|
||||
salt_chars = 'ab'
|
||||
|
||||
def norm_salt(**k):
|
||||
return d1(**k).salt
|
||||
|
||||
def gen_salt(sz, **k):
|
||||
return d1.using(salt_size=sz, **k)(use_defaults=True).salt
|
||||
|
||||
salts2 = _makelang('ab', 2)
|
||||
salts3 = _makelang('ab', 3)
|
||||
salts4 = _makelang('ab', 4)
|
||||
|
||||
# check salt=None
|
||||
self.assertRaises(TypeError, norm_salt)
|
||||
self.assertRaises(TypeError, norm_salt, salt=None)
|
||||
self.assertIn(norm_salt(use_defaults=True), salts3)
|
||||
|
||||
# check explicit salts
|
||||
with warnings.catch_warnings(record=True) as wlog:
|
||||
|
||||
# check too-small salts
|
||||
self.assertRaises(ValueError, norm_salt, salt='')
|
||||
self.assertRaises(ValueError, norm_salt, salt='a')
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
# check correct salts
|
||||
self.assertEqual(norm_salt(salt='ab'), 'ab')
|
||||
self.assertEqual(norm_salt(salt='aba'), 'aba')
|
||||
self.assertEqual(norm_salt(salt='abba'), 'abba')
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
# check too-large salts
|
||||
self.assertRaises(ValueError, norm_salt, salt='aaaabb')
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
# check generated salts
|
||||
with warnings.catch_warnings(record=True) as wlog:
|
||||
|
||||
# check too-small salt size
|
||||
self.assertRaises(ValueError, gen_salt, 0)
|
||||
self.assertRaises(ValueError, gen_salt, 1)
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
# check correct salt size
|
||||
self.assertIn(gen_salt(2), salts2)
|
||||
self.assertIn(gen_salt(3), salts3)
|
||||
self.assertIn(gen_salt(4), salts4)
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
# check too-large salt size
|
||||
self.assertRaises(ValueError, gen_salt, 5)
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
self.assertIn(gen_salt(5, relaxed=True), salts4)
|
||||
self.consumeWarningList(wlog, ["salt_size.*above max_salt_size"])
|
||||
|
||||
# test with max_salt_size=None
|
||||
del d1.max_salt_size
|
||||
with self.assertWarningList([]):
|
||||
self.assertEqual(len(gen_salt(None)), 3)
|
||||
self.assertEqual(len(gen_salt(5)), 5)
|
||||
|
||||
# TODO: test HasRawSalt mixin
|
||||
|
||||
def test_30_init_rounds(self):
|
||||
"""test GenericHandler + HasRounds mixin"""
|
||||
# setup helpers
|
||||
class d1(uh.HasRounds, uh.GenericHandler):
|
||||
name = 'd1'
|
||||
setting_kwds = ('rounds',)
|
||||
min_rounds = 1
|
||||
max_rounds = 3
|
||||
default_rounds = 2
|
||||
|
||||
# NOTE: really is testing _init_rounds(), could dup to test _norm_rounds() via .replace
|
||||
def norm_rounds(**k):
|
||||
return d1(**k).rounds
|
||||
|
||||
# check rounds=None
|
||||
self.assertRaises(TypeError, norm_rounds)
|
||||
self.assertRaises(TypeError, norm_rounds, rounds=None)
|
||||
self.assertEqual(norm_rounds(use_defaults=True), 2)
|
||||
|
||||
# check rounds=non int
|
||||
self.assertRaises(TypeError, norm_rounds, rounds=1.5)
|
||||
|
||||
# check explicit rounds
|
||||
with warnings.catch_warnings(record=True) as wlog:
|
||||
# too small
|
||||
self.assertRaises(ValueError, norm_rounds, rounds=0)
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
# just right
|
||||
self.assertEqual(norm_rounds(rounds=1), 1)
|
||||
self.assertEqual(norm_rounds(rounds=2), 2)
|
||||
self.assertEqual(norm_rounds(rounds=3), 3)
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
# too large
|
||||
self.assertRaises(ValueError, norm_rounds, rounds=4)
|
||||
self.consumeWarningList(wlog)
|
||||
|
||||
# check no default rounds
|
||||
d1.default_rounds = None
|
||||
self.assertRaises(TypeError, norm_rounds, use_defaults=True)
|
||||
|
||||
def test_40_backends(self):
|
||||
"""test GenericHandler + HasManyBackends mixin"""
|
||||
class d1(uh.HasManyBackends, uh.GenericHandler):
|
||||
name = 'd1'
|
||||
setting_kwds = ()
|
||||
|
||||
backends = ("a", "b")
|
||||
|
||||
_enable_a = False
|
||||
_enable_b = False
|
||||
|
||||
@classmethod
|
||||
def _load_backend_a(cls):
|
||||
if cls._enable_a:
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_a)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _load_backend_b(cls):
|
||||
if cls._enable_b:
|
||||
cls._set_calc_checksum_backend(cls._calc_checksum_b)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _calc_checksum_a(self, secret):
|
||||
return 'a'
|
||||
|
||||
def _calc_checksum_b(self, secret):
|
||||
return 'b'
|
||||
|
||||
# test no backends
|
||||
self.assertRaises(MissingBackendError, d1.get_backend)
|
||||
self.assertRaises(MissingBackendError, d1.set_backend)
|
||||
self.assertRaises(MissingBackendError, d1.set_backend, 'any')
|
||||
self.assertRaises(MissingBackendError, d1.set_backend, 'default')
|
||||
self.assertFalse(d1.has_backend())
|
||||
|
||||
# enable 'b' backend
|
||||
d1._enable_b = True
|
||||
|
||||
# test lazy load
|
||||
obj = d1()
|
||||
self.assertEqual(obj._calc_checksum('s'), 'b')
|
||||
|
||||
# test repeat load
|
||||
d1.set_backend('b')
|
||||
d1.set_backend('any')
|
||||
self.assertEqual(obj._calc_checksum('s'), 'b')
|
||||
|
||||
# test unavailable
|
||||
self.assertRaises(MissingBackendError, d1.set_backend, 'a')
|
||||
self.assertTrue(d1.has_backend('b'))
|
||||
self.assertFalse(d1.has_backend('a'))
|
||||
|
||||
# enable 'a' backend also
|
||||
d1._enable_a = True
|
||||
|
||||
# test explicit
|
||||
self.assertTrue(d1.has_backend())
|
||||
d1.set_backend('a')
|
||||
self.assertEqual(obj._calc_checksum('s'), 'a')
|
||||
|
||||
# test unknown backend
|
||||
self.assertRaises(ValueError, d1.set_backend, 'c')
|
||||
self.assertRaises(ValueError, d1.has_backend, 'c')
|
||||
|
||||
# test error thrown if _has & _load are mixed
|
||||
d1.set_backend("b") # switch away from 'a' so next call actually checks loader
|
||||
class d2(d1):
|
||||
_has_backend_a = True
|
||||
self.assertRaises(AssertionError, d2.has_backend, "a")
|
||||
|
||||
def test_41_backends(self):
|
||||
"""test GenericHandler + HasManyBackends mixin (deprecated api)"""
|
||||
warnings.filterwarnings("ignore",
|
||||
category=DeprecationWarning,
|
||||
message=r".* support for \._has_backend_.* is deprecated.*",
|
||||
)
|
||||
|
||||
class d1(uh.HasManyBackends, uh.GenericHandler):
|
||||
name = 'd1'
|
||||
setting_kwds = ()
|
||||
|
||||
backends = ("a", "b")
|
||||
|
||||
_has_backend_a = False
|
||||
_has_backend_b = False
|
||||
|
||||
def _calc_checksum_a(self, secret):
|
||||
return 'a'
|
||||
|
||||
def _calc_checksum_b(self, secret):
|
||||
return 'b'
|
||||
|
||||
# test no backends
|
||||
self.assertRaises(MissingBackendError, d1.get_backend)
|
||||
self.assertRaises(MissingBackendError, d1.set_backend)
|
||||
self.assertRaises(MissingBackendError, d1.set_backend, 'any')
|
||||
self.assertRaises(MissingBackendError, d1.set_backend, 'default')
|
||||
self.assertFalse(d1.has_backend())
|
||||
|
||||
# enable 'b' backend
|
||||
d1._has_backend_b = True
|
||||
|
||||
# test lazy load
|
||||
obj = d1()
|
||||
self.assertEqual(obj._calc_checksum('s'), 'b')
|
||||
|
||||
# test repeat load
|
||||
d1.set_backend('b')
|
||||
d1.set_backend('any')
|
||||
self.assertEqual(obj._calc_checksum('s'), 'b')
|
||||
|
||||
# test unavailable
|
||||
self.assertRaises(MissingBackendError, d1.set_backend, 'a')
|
||||
self.assertTrue(d1.has_backend('b'))
|
||||
self.assertFalse(d1.has_backend('a'))
|
||||
|
||||
# enable 'a' backend also
|
||||
d1._has_backend_a = True
|
||||
|
||||
# test explicit
|
||||
self.assertTrue(d1.has_backend())
|
||||
d1.set_backend('a')
|
||||
self.assertEqual(obj._calc_checksum('s'), 'a')
|
||||
|
||||
# test unknown backend
|
||||
self.assertRaises(ValueError, d1.set_backend, 'c')
|
||||
self.assertRaises(ValueError, d1.has_backend, 'c')
|
||||
|
||||
def test_50_norm_ident(self):
|
||||
"""test GenericHandler + HasManyIdents"""
|
||||
# setup helpers
|
||||
class d1(uh.HasManyIdents, uh.GenericHandler):
|
||||
name = 'd1'
|
||||
setting_kwds = ('ident',)
|
||||
default_ident = u("!A")
|
||||
ident_values = (u("!A"), u("!B"))
|
||||
ident_aliases = { u("A"): u("!A")}
|
||||
|
||||
def norm_ident(**k):
|
||||
return d1(**k).ident
|
||||
|
||||
# check ident=None
|
||||
self.assertRaises(TypeError, norm_ident)
|
||||
self.assertRaises(TypeError, norm_ident, ident=None)
|
||||
self.assertEqual(norm_ident(use_defaults=True), u('!A'))
|
||||
|
||||
# check valid idents
|
||||
self.assertEqual(norm_ident(ident=u('!A')), u('!A'))
|
||||
self.assertEqual(norm_ident(ident=u('!B')), u('!B'))
|
||||
self.assertRaises(ValueError, norm_ident, ident=u('!C'))
|
||||
|
||||
# check aliases
|
||||
self.assertEqual(norm_ident(ident=u('A')), u('!A'))
|
||||
|
||||
# check invalid idents
|
||||
self.assertRaises(ValueError, norm_ident, ident=u('B'))
|
||||
|
||||
# check identify is honoring ident system
|
||||
self.assertTrue(d1.identify(u("!Axxx")))
|
||||
self.assertTrue(d1.identify(u("!Bxxx")))
|
||||
self.assertFalse(d1.identify(u("!Cxxx")))
|
||||
self.assertFalse(d1.identify(u("A")))
|
||||
self.assertFalse(d1.identify(u("")))
|
||||
self.assertRaises(TypeError, d1.identify, None)
|
||||
self.assertRaises(TypeError, d1.identify, 1)
|
||||
|
||||
# check default_ident missing is detected.
|
||||
d1.default_ident = None
|
||||
self.assertRaises(AssertionError, norm_ident, use_defaults=True)
|
||||
|
||||
#===================================================================
|
||||
# experimental - the following methods are not finished or tested,
|
||||
# but way work correctly for some hashes
|
||||
#===================================================================
|
||||
def test_91_parsehash(self):
|
||||
"""test parsehash()"""
|
||||
# NOTE: this just tests some existing GenericHandler classes
|
||||
from passlib import hash
|
||||
|
||||
#
|
||||
# parsehash()
|
||||
#
|
||||
|
||||
# simple hash w/ salt
|
||||
result = hash.des_crypt.parsehash("OgAwTx2l6NADI")
|
||||
self.assertEqual(result, {'checksum': u('AwTx2l6NADI'), 'salt': u('Og')})
|
||||
|
||||
# parse rounds and extra implicit_rounds flag
|
||||
h = '$5$LKO/Ute40T3FNF95$U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9'
|
||||
s = u('LKO/Ute40T3FNF95')
|
||||
c = u('U0prpBQd4PloSGU0pnpM4z9wKn4vZ1.jsrzQfPqxph9')
|
||||
result = hash.sha256_crypt.parsehash(h)
|
||||
self.assertEqual(result, dict(salt=s, rounds=5000,
|
||||
implicit_rounds=True, checksum=c))
|
||||
|
||||
# omit checksum
|
||||
result = hash.sha256_crypt.parsehash(h, checksum=False)
|
||||
self.assertEqual(result, dict(salt=s, rounds=5000, implicit_rounds=True))
|
||||
|
||||
# sanitize
|
||||
result = hash.sha256_crypt.parsehash(h, sanitize=True)
|
||||
self.assertEqual(result, dict(rounds=5000, implicit_rounds=True,
|
||||
salt=u('LK**************'),
|
||||
checksum=u('U0pr***************************************')))
|
||||
|
||||
# parse w/o implicit rounds flag
|
||||
result = hash.sha256_crypt.parsehash('$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3')
|
||||
self.assertEqual(result, dict(
|
||||
checksum=u('YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3'),
|
||||
salt=u('uy/jIAhCetNCTtb0'),
|
||||
rounds=10428,
|
||||
))
|
||||
|
||||
# parsing of raw checksums & salts
|
||||
h1 = '$pbkdf2$60000$DoEwpvQeA8B4T.k951yLUQ$O26Y3/NJEiLCVaOVPxGXshyjW8k'
|
||||
result = hash.pbkdf2_sha1.parsehash(h1)
|
||||
self.assertEqual(result, dict(
|
||||
checksum=b';n\x98\xdf\xf3I\x12"\xc2U\xa3\x95?\x11\x97\xb2\x1c\xa3[\xc9',
|
||||
rounds=60000,
|
||||
salt=b'\x0e\x810\xa6\xf4\x1e\x03\xc0xO\xe9=\xe7\\\x8bQ',
|
||||
))
|
||||
|
||||
# sanitizing of raw checksums & salts
|
||||
result = hash.pbkdf2_sha1.parsehash(h1, sanitize=True)
|
||||
self.assertEqual(result, dict(
|
||||
checksum=u('O26************************'),
|
||||
rounds=60000,
|
||||
salt=u('Do********************'),
|
||||
))
|
||||
|
||||
def test_92_bitsize(self):
|
||||
"""test bitsize()"""
|
||||
# NOTE: this just tests some existing GenericHandler classes
|
||||
from passlib import hash
|
||||
|
||||
# no rounds
|
||||
self.assertEqual(hash.des_crypt.bitsize(),
|
||||
{'checksum': 66, 'salt': 12})
|
||||
|
||||
# log2 rounds
|
||||
self.assertEqual(hash.bcrypt.bitsize(),
|
||||
{'checksum': 186, 'salt': 132})
|
||||
|
||||
# linear rounds
|
||||
# NOTE: +3 comes from int(math.log(.1,2)),
|
||||
# where 0.1 = 10% = default allowed variation in rounds
|
||||
self.patchAttr(hash.sha256_crypt, "default_rounds", 1 << (14 + 3))
|
||||
self.assertEqual(hash.sha256_crypt.bitsize(),
|
||||
{'checksum': 258, 'rounds': 14, 'salt': 96})
|
||||
|
||||
# raw checksum
|
||||
self.patchAttr(hash.pbkdf2_sha1, "default_rounds", 1 << (13 + 3))
|
||||
self.assertEqual(hash.pbkdf2_sha1.bitsize(),
|
||||
{'checksum': 160, 'rounds': 13, 'salt': 128})
|
||||
|
||||
# TODO: handle fshp correctly, and other glitches noted in code.
|
||||
##self.assertEqual(hash.fshp.bitsize(variant=1),
|
||||
## {'checksum': 256, 'rounds': 13, 'salt': 128})
|
||||
|
||||
#===================================================================
|
||||
# eoc
|
||||
#===================================================================
|
||||
|
||||
#=============================================================================
|
||||
# PrefixWrapper
|
||||
#=============================================================================
|
||||
class dummy_handler_in_registry(object):
|
||||
"""context manager that inserts dummy handler in registry"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.dummy = type('dummy_' + name, (uh.GenericHandler,), dict(
|
||||
name=name,
|
||||
setting_kwds=(),
|
||||
))
|
||||
|
||||
def __enter__(self):
|
||||
from passlib import registry
|
||||
registry._unload_handler_name(self.name, locations=False)
|
||||
registry.register_crypt_handler(self.dummy)
|
||||
assert registry.get_crypt_handler(self.name) is self.dummy
|
||||
return self.dummy
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
from passlib import registry
|
||||
registry._unload_handler_name(self.name, locations=False)
|
||||
|
||||
class PrefixWrapperTest(TestCase):
|
||||
"""test PrefixWrapper class"""
|
||||
|
||||
def test_00_lazy_loading(self):
|
||||
"""test PrefixWrapper lazy loading of handler"""
|
||||
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}", lazy=True)
|
||||
|
||||
# check base state
|
||||
self.assertEqual(d1._wrapped_name, "ldap_md5")
|
||||
self.assertIs(d1._wrapped_handler, None)
|
||||
|
||||
# check loading works
|
||||
self.assertIs(d1.wrapped, ldap_md5)
|
||||
self.assertIs(d1._wrapped_handler, ldap_md5)
|
||||
|
||||
# replace w/ wrong handler, make sure doesn't reload w/ dummy
|
||||
with dummy_handler_in_registry("ldap_md5") as dummy:
|
||||
self.assertIs(d1.wrapped, ldap_md5)
|
||||
|
||||
def test_01_active_loading(self):
|
||||
"""test PrefixWrapper active loading of handler"""
|
||||
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
|
||||
|
||||
# check base state
|
||||
self.assertEqual(d1._wrapped_name, "ldap_md5")
|
||||
self.assertIs(d1._wrapped_handler, ldap_md5)
|
||||
self.assertIs(d1.wrapped, ldap_md5)
|
||||
|
||||
# replace w/ wrong handler, make sure doesn't reload w/ dummy
|
||||
with dummy_handler_in_registry("ldap_md5") as dummy:
|
||||
self.assertIs(d1.wrapped, ldap_md5)
|
||||
|
||||
def test_02_explicit(self):
|
||||
"""test PrefixWrapper with explicitly specified handler"""
|
||||
|
||||
d1 = uh.PrefixWrapper("d1", ldap_md5, "{XXX}", "{MD5}")
|
||||
|
||||
# check base state
|
||||
self.assertEqual(d1._wrapped_name, None)
|
||||
self.assertIs(d1._wrapped_handler, ldap_md5)
|
||||
self.assertIs(d1.wrapped, ldap_md5)
|
||||
|
||||
# replace w/ wrong handler, make sure doesn't reload w/ dummy
|
||||
with dummy_handler_in_registry("ldap_md5") as dummy:
|
||||
self.assertIs(d1.wrapped, ldap_md5)
|
||||
|
||||
def test_10_wrapped_attributes(self):
|
||||
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
|
||||
self.assertEqual(d1.name, "d1")
|
||||
self.assertIs(d1.setting_kwds, ldap_md5.setting_kwds)
|
||||
self.assertFalse('max_rounds' in dir(d1))
|
||||
|
||||
d2 = uh.PrefixWrapper("d2", "sha256_crypt", "{XXX}")
|
||||
self.assertIs(d2.setting_kwds, sha256_crypt.setting_kwds)
|
||||
self.assertTrue('max_rounds' in dir(d2))
|
||||
|
||||
def test_11_wrapped_methods(self):
|
||||
d1 = uh.PrefixWrapper("d1", "ldap_md5", "{XXX}", "{MD5}")
|
||||
dph = "{XXX}X03MO1qnZdYdgyfeuILPmQ=="
|
||||
lph = "{MD5}X03MO1qnZdYdgyfeuILPmQ=="
|
||||
|
||||
# genconfig
|
||||
self.assertEqual(d1.genconfig(), '{XXX}1B2M2Y8AsgTpgAmY7PhCfg==')
|
||||
|
||||
# genhash
|
||||
self.assertRaises(TypeError, d1.genhash, "password", None)
|
||||
self.assertEqual(d1.genhash("password", dph), dph)
|
||||
self.assertRaises(ValueError, d1.genhash, "password", lph)
|
||||
|
||||
# hash
|
||||
self.assertEqual(d1.hash("password"), dph)
|
||||
|
||||
# identify
|
||||
self.assertTrue(d1.identify(dph))
|
||||
self.assertFalse(d1.identify(lph))
|
||||
|
||||
# verify
|
||||
self.assertRaises(ValueError, d1.verify, "password", lph)
|
||||
self.assertTrue(d1.verify("password", dph))
|
||||
|
||||
def test_12_ident(self):
|
||||
# test ident is proxied
|
||||
h = uh.PrefixWrapper("h2", "ldap_md5", "{XXX}")
|
||||
self.assertEqual(h.ident, u("{XXX}{MD5}"))
|
||||
self.assertIs(h.ident_values, None)
|
||||
|
||||
# test lack of ident means no proxy
|
||||
h = uh.PrefixWrapper("h2", "des_crypt", "{XXX}")
|
||||
self.assertIs(h.ident, None)
|
||||
self.assertIs(h.ident_values, None)
|
||||
|
||||
# test orig_prefix disabled ident proxy
|
||||
h = uh.PrefixWrapper("h1", "ldap_md5", "{XXX}", "{MD5}")
|
||||
self.assertIs(h.ident, None)
|
||||
self.assertIs(h.ident_values, None)
|
||||
|
||||
# test custom ident overrides default
|
||||
h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{X")
|
||||
self.assertEqual(h.ident, u("{X"))
|
||||
self.assertIs(h.ident_values, None)
|
||||
|
||||
# test custom ident must match
|
||||
h = uh.PrefixWrapper("h3", "ldap_md5", "{XXX}", ident="{XXX}A")
|
||||
self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5",
|
||||
"{XXX}", ident="{XY")
|
||||
self.assertRaises(ValueError, uh.PrefixWrapper, "h3", "ldap_md5",
|
||||
"{XXX}", ident="{XXXX")
|
||||
|
||||
# test ident_values is proxied
|
||||
h = uh.PrefixWrapper("h4", "phpass", "{XXX}")
|
||||
self.assertIs(h.ident, None)
|
||||
self.assertEqual(h.ident_values, (u("{XXX}$P$"), u("{XXX}$H$")))
|
||||
|
||||
# test ident=True means use prefix even if hash has no ident.
|
||||
h = uh.PrefixWrapper("h5", "des_crypt", "{XXX}", ident=True)
|
||||
self.assertEqual(h.ident, u("{XXX}"))
|
||||
self.assertIs(h.ident_values, None)
|
||||
|
||||
# ... but requires prefix
|
||||
self.assertRaises(ValueError, uh.PrefixWrapper, "h6", "des_crypt", ident=True)
|
||||
|
||||
# orig_prefix + HasManyIdent - warning
|
||||
with self.assertWarningList("orig_prefix.*may not work correctly"):
|
||||
h = uh.PrefixWrapper("h7", "phpass", orig_prefix="$", prefix="?")
|
||||
self.assertEqual(h.ident_values, None) # TODO: should output (u("?P$"), u("?H$")))
|
||||
self.assertEqual(h.ident, None)
|
||||
|
||||
def test_13_repr(self):
|
||||
"""test repr()"""
|
||||
h = uh.PrefixWrapper("h2", "md5_crypt", "{XXX}", orig_prefix="$1$")
|
||||
self.assertRegex(repr(h),
|
||||
r"""(?x)^PrefixWrapper\(
|
||||
['"]h2['"],\s+
|
||||
['"]md5_crypt['"],\s+
|
||||
prefix=u?["']{XXX}['"],\s+
|
||||
orig_prefix=u?["']\$1\$['"]
|
||||
\)$""")
|
||||
|
||||
def test_14_bad_hash(self):
|
||||
"""test orig_prefix sanity check"""
|
||||
# shoudl throw InvalidHashError if wrapped hash doesn't begin
|
||||
# with orig_prefix.
|
||||
h = uh.PrefixWrapper("h2", "md5_crypt", orig_prefix="$6$")
|
||||
self.assertRaises(ValueError, h.hash, 'test')
|
||||
|
||||
#=============================================================================
|
||||
# sample algorithms - these serve as known quantities
|
||||
# to test the unittests themselves, as well as other
|
||||
# parts of passlib. they shouldn't be used as actual password schemes.
|
||||
#=============================================================================
|
||||
class UnsaltedHash(uh.StaticHandler):
|
||||
"""test algorithm which lacks a salt"""
|
||||
name = "unsalted_test_hash"
|
||||
checksum_chars = uh.LOWER_HEX_CHARS
|
||||
checksum_size = 40
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
data = b"boblious" + secret
|
||||
return str_to_uascii(hashlib.sha1(data).hexdigest())
|
||||
|
||||
class SaltedHash(uh.HasSalt, uh.GenericHandler):
|
||||
"""test algorithm with a salt"""
|
||||
name = "salted_test_hash"
|
||||
setting_kwds = ("salt",)
|
||||
|
||||
min_salt_size = 2
|
||||
max_salt_size = 4
|
||||
checksum_size = 40
|
||||
salt_chars = checksum_chars = uh.LOWER_HEX_CHARS
|
||||
|
||||
_hash_regex = re.compile(u("^@salt[0-9a-f]{42,44}$"))
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, hash):
|
||||
if not cls.identify(hash):
|
||||
raise uh.exc.InvalidHashError(cls)
|
||||
if isinstance(hash, bytes):
|
||||
hash = hash.decode("ascii")
|
||||
return cls(salt=hash[5:-40], checksum=hash[-40:])
|
||||
|
||||
def to_string(self):
|
||||
hash = u("@salt%s%s") % (self.salt, self.checksum)
|
||||
return uascii_to_str(hash)
|
||||
|
||||
def _calc_checksum(self, secret):
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode("utf-8")
|
||||
data = self.salt.encode("ascii") + secret + self.salt.encode("ascii")
|
||||
return str_to_uascii(hashlib.sha1(data).hexdigest())
|
||||
|
||||
#=============================================================================
|
||||
# test sample algorithms - really a self-test of HandlerCase
|
||||
#=============================================================================
|
||||
|
||||
# TODO: provide data samples for algorithms
|
||||
# (positive knowns, negative knowns, invalid identify)
|
||||
|
||||
UPASS_TEMP = u('\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2')
|
||||
|
||||
class UnsaltedHashTest(HandlerCase):
|
||||
handler = UnsaltedHash
|
||||
|
||||
known_correct_hashes = [
|
||||
("password", "61cfd32684c47de231f1f982c214e884133762c0"),
|
||||
(UPASS_TEMP, '96b329d120b97ff81ada770042e44ba87343ad2b'),
|
||||
]
|
||||
|
||||
def test_bad_kwds(self):
|
||||
self.assertRaises(TypeError, UnsaltedHash, salt='x')
|
||||
self.assertRaises(TypeError, UnsaltedHash.genconfig, rounds=1)
|
||||
|
||||
class SaltedHashTest(HandlerCase):
|
||||
handler = SaltedHash
|
||||
|
||||
known_correct_hashes = [
|
||||
("password", '@salt77d71f8fe74f314dac946766c1ac4a2a58365482c0'),
|
||||
(UPASS_TEMP, '@salt9f978a9bfe360d069b0c13f2afecd570447407fa7e48'),
|
||||
]
|
||||
|
||||
def test_bad_kwds(self):
|
||||
stub = SaltedHash(use_defaults=True)._stub_checksum
|
||||
self.assertRaises(TypeError, SaltedHash, checksum=stub, salt=None)
|
||||
self.assertRaises(ValueError, SaltedHash, checksum=stub, salt='xxx')
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
passlib.tests -- tests for passlib.utils.md4
|
||||
|
||||
.. warning::
|
||||
|
||||
This module & it's functions have been deprecated, and superceded
|
||||
by the functions in passlib.crypto. This file is being maintained
|
||||
until the deprecated functions are removed, and is only present prevent
|
||||
historical regressions up to that point. New and more thorough testing
|
||||
is being done by the replacement tests in ``test_utils_crypto_builtin_md4``.
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import warnings
|
||||
# site
|
||||
# pkg
|
||||
# module
|
||||
from passlib.tests.test_crypto_builtin_md4 import _Common_MD4_Test
|
||||
# local
|
||||
__all__ = [
|
||||
"Legacy_MD4_Test",
|
||||
]
|
||||
#=============================================================================
|
||||
# test pure-python MD4 implementation
|
||||
#=============================================================================
|
||||
class Legacy_MD4_Test(_Common_MD4_Test):
|
||||
descriptionPrefix = "passlib.utils.md4.md4()"
|
||||
|
||||
def setUp(self):
|
||||
super(Legacy_MD4_Test, self).setUp()
|
||||
warnings.filterwarnings("ignore", ".*passlib.utils.md4.*deprecated", DeprecationWarning)
|
||||
|
||||
def get_md4_const(self):
|
||||
from passlib.utils.md4 import md4
|
||||
return md4
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,323 @@
|
||||
"""
|
||||
passlib.tests -- tests for passlib.utils.pbkdf2
|
||||
|
||||
.. warning::
|
||||
|
||||
This module & it's functions have been deprecated, and superceded
|
||||
by the functions in passlib.crypto. This file is being maintained
|
||||
until the deprecated functions are removed, and is only present prevent
|
||||
historical regressions up to that point. New and more thorough testing
|
||||
is being done by the replacement tests in ``test_utils_crypto.py``.
|
||||
"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
from __future__ import with_statement
|
||||
# core
|
||||
import hashlib
|
||||
import warnings
|
||||
# site
|
||||
# pkg
|
||||
# module
|
||||
from passlib.utils.compat import u, JYTHON
|
||||
from passlib.tests.utils import TestCase, hb
|
||||
|
||||
#=============================================================================
|
||||
# test assorted crypto helpers
|
||||
#=============================================================================
|
||||
class UtilsTest(TestCase):
|
||||
"""test various utils functions"""
|
||||
descriptionPrefix = "passlib.utils.pbkdf2"
|
||||
|
||||
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"),
|
||||
("ripemd160", "ripemd-160", "SCRAM-RIPEMD-160", "RIPEmd160",
|
||||
# NOTE: there was an older "RIPEMD" & "RIPEMD-128", but python treates "RIPEMD"
|
||||
# as alias for "RIPEMD-160"
|
||||
"ripemd", "SCRAM-RIPEMD"),
|
||||
("test128", "test-128", "TEST128"),
|
||||
("test2", "test2", "TEST-2"),
|
||||
("test3_128", "test3-128", "TEST-3-128"),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(UtilsTest, self).setUp()
|
||||
warnings.filterwarnings("ignore", ".*passlib.utils.pbkdf2.*deprecated", DeprecationWarning)
|
||||
|
||||
def test_norm_hash_name(self):
|
||||
"""norm_hash_name()"""
|
||||
from itertools import chain
|
||||
from passlib.utils.pbkdf2 import norm_hash_name
|
||||
from passlib.crypto.digest import _known_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 warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", '.*unknown hash')
|
||||
for row in chain(_known_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))
|
||||
|
||||
#=============================================================================
|
||||
# test PBKDF1 support
|
||||
#=============================================================================
|
||||
class Pbkdf1_Test(TestCase):
|
||||
"""test kdf helpers"""
|
||||
descriptionPrefix = "passlib.utils.pbkdf2.pbkdf1()"
|
||||
|
||||
pbkdf1_tests = [
|
||||
# (password, salt, rounds, keylen, hash, result)
|
||||
|
||||
#
|
||||
# from http://www.di-mgt.com.au/cryptoKDFs.html
|
||||
#
|
||||
(b'password', hb('78578E5A5D63CB06'), 1000, 16, 'sha1', hb('dc19847e05c64d2faf10ebfb4a3d2a20')),
|
||||
|
||||
#
|
||||
# custom
|
||||
#
|
||||
(b'password', b'salt', 1000, 0, 'md5', b''),
|
||||
(b'password', b'salt', 1000, 1, 'md5', hb('84')),
|
||||
(b'password', b'salt', 1000, 8, 'md5', hb('8475c6a8531a5d27')),
|
||||
(b'password', b'salt', 1000, 16, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
|
||||
(b'password', b'salt', 1000, None, 'md5', hb('8475c6a8531a5d27e386cd496457812c')),
|
||||
(b'password', b'salt', 1000, None, 'sha1', hb('4a8fd48e426ed081b535be5769892fa396293efb')),
|
||||
]
|
||||
if not JYTHON:
|
||||
pbkdf1_tests.append(
|
||||
(b'password', b'salt', 1000, None, 'md4', hb('f7f2e91100a8f96190f2dd177cb26453'))
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(Pbkdf1_Test, self).setUp()
|
||||
warnings.filterwarnings("ignore", ".*passlib.utils.pbkdf2.*deprecated", DeprecationWarning)
|
||||
|
||||
def test_known(self):
|
||||
"""test reference vectors"""
|
||||
from passlib.utils.pbkdf2 import pbkdf1
|
||||
for secret, salt, rounds, keylen, digest, correct in self.pbkdf1_tests:
|
||||
result = pbkdf1(secret, salt, rounds, keylen, digest)
|
||||
self.assertEqual(result, correct)
|
||||
|
||||
def test_border(self):
|
||||
"""test border cases"""
|
||||
from passlib.utils.pbkdf2 import pbkdf1
|
||||
def helper(secret=b'secret', salt=b'salt', rounds=1, keylen=1, hash='md5'):
|
||||
return pbkdf1(secret, salt, rounds, keylen, hash)
|
||||
helper()
|
||||
|
||||
# salt/secret wrong type
|
||||
self.assertRaises(TypeError, helper, secret=1)
|
||||
self.assertRaises(TypeError, helper, salt=1)
|
||||
|
||||
# non-existent hashes
|
||||
self.assertRaises(ValueError, helper, hash='missing')
|
||||
|
||||
# rounds < 1 and wrong type
|
||||
self.assertRaises(ValueError, helper, rounds=0)
|
||||
self.assertRaises(TypeError, helper, rounds='1')
|
||||
|
||||
# keylen < 0, keylen > block_size, and wrong type
|
||||
self.assertRaises(ValueError, helper, keylen=-1)
|
||||
self.assertRaises(ValueError, helper, keylen=17, hash='md5')
|
||||
self.assertRaises(TypeError, helper, keylen='1')
|
||||
|
||||
#=============================================================================
|
||||
# test PBKDF2 support
|
||||
#=============================================================================
|
||||
class Pbkdf2_Test(TestCase):
|
||||
"""test pbkdf2() support"""
|
||||
descriptionPrefix = "passlib.utils.pbkdf2.pbkdf2()"
|
||||
|
||||
pbkdf2_test_vectors = [
|
||||
# (result, secret, salt, rounds, keylen, prf="sha1")
|
||||
|
||||
#
|
||||
# from rfc 3962
|
||||
#
|
||||
|
||||
# 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
|
||||
),
|
||||
|
||||
#
|
||||
# from rfc 6070
|
||||
#
|
||||
(
|
||||
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,
|
||||
),
|
||||
|
||||
#
|
||||
# from example in http://grub.enbug.org/Authentication
|
||||
#
|
||||
(
|
||||
hb("887CFF169EA8335235D8004242AA7D6187A41E3187DF0CE14E256D85ED"
|
||||
"97A97357AAA8FF0A3871AB9EEFF458392F462F495487387F685B7472FC"
|
||||
"6C29E293F0A0"),
|
||||
b"hello",
|
||||
hb("9290F727ED06C38BA4549EF7DE25CF5642659211B7FC076F2D28FEFD71"
|
||||
"784BB8D8F6FB244A8CC5C06240631B97008565A120764C0EE9C2CB0073"
|
||||
"994D79080136"),
|
||||
10000, 64, "hmac-sha512"
|
||||
),
|
||||
|
||||
#
|
||||
# custom
|
||||
#
|
||||
(
|
||||
hb('e248fb6b13365146f8ac6307cc222812'),
|
||||
b"secret", b"salt", 10, 16, "hmac-sha1",
|
||||
),
|
||||
(
|
||||
hb('e248fb6b13365146f8ac6307cc2228127872da6d'),
|
||||
b"secret", b"salt", 10, None, "hmac-sha1",
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(Pbkdf2_Test, self).setUp()
|
||||
warnings.filterwarnings("ignore", ".*passlib.utils.pbkdf2.*deprecated", DeprecationWarning)
|
||||
|
||||
def test_known(self):
|
||||
"""test reference vectors"""
|
||||
from passlib.utils.pbkdf2 import pbkdf2
|
||||
for row in self.pbkdf2_test_vectors:
|
||||
correct, secret, salt, rounds, keylen = row[:5]
|
||||
prf = row[5] if len(row) == 6 else "hmac-sha1"
|
||||
result = pbkdf2(secret, salt, rounds, keylen, prf)
|
||||
self.assertEqual(result, correct)
|
||||
|
||||
def test_border(self):
|
||||
"""test border cases"""
|
||||
from passlib.utils.pbkdf2 import pbkdf2
|
||||
def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, prf="hmac-sha1"):
|
||||
return pbkdf2(secret, salt, rounds, keylen, prf)
|
||||
helper()
|
||||
|
||||
# invalid rounds
|
||||
self.assertRaises(ValueError, helper, rounds=-1)
|
||||
self.assertRaises(ValueError, helper, rounds=0)
|
||||
self.assertRaises(TypeError, helper, rounds='x')
|
||||
|
||||
# invalid keylen
|
||||
self.assertRaises(ValueError, helper, keylen=-1)
|
||||
self.assertRaises(ValueError, helper, keylen=0)
|
||||
helper(keylen=1)
|
||||
self.assertRaises(OverflowError, helper, keylen=20*(2**32-1)+1)
|
||||
self.assertRaises(TypeError, helper, keylen='x')
|
||||
|
||||
# invalid secret/salt type
|
||||
self.assertRaises(TypeError, helper, salt=5)
|
||||
self.assertRaises(TypeError, helper, secret=5)
|
||||
|
||||
# invalid hash
|
||||
self.assertRaises(ValueError, helper, prf='hmac-foo')
|
||||
self.assertRaises(NotImplementedError, helper, prf='foo')
|
||||
self.assertRaises(TypeError, helper, prf=5)
|
||||
|
||||
def test_default_keylen(self):
|
||||
"""test keylen==None"""
|
||||
from passlib.utils.pbkdf2 import pbkdf2
|
||||
def helper(secret=b'password', salt=b'salt', rounds=1, keylen=None, prf="hmac-sha1"):
|
||||
return pbkdf2(secret, salt, rounds, keylen, prf)
|
||||
self.assertEqual(len(helper(prf='hmac-sha1')), 20)
|
||||
self.assertEqual(len(helper(prf='hmac-sha256')), 32)
|
||||
|
||||
def test_custom_prf(self):
|
||||
"""test custom prf function"""
|
||||
from passlib.utils.pbkdf2 import pbkdf2
|
||||
def prf(key, msg):
|
||||
return hashlib.md5(key+msg+b'fooey').digest()
|
||||
self.assertRaises(NotImplementedError, pbkdf2, b'secret', b'salt', 1000, 20, prf)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,50 @@
|
||||
"""tests for passlib.win32 -- (c) Assurance Technologies 2003-2009"""
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import warnings
|
||||
# site
|
||||
# pkg
|
||||
from passlib.tests.utils import TestCase
|
||||
# module
|
||||
from passlib.utils.compat import u
|
||||
|
||||
#=============================================================================
|
||||
#
|
||||
#=============================================================================
|
||||
class UtilTest(TestCase):
|
||||
"""test util funcs in passlib.win32"""
|
||||
|
||||
##test hashes from http://msdn.microsoft.com/en-us/library/cc245828(v=prot.10).aspx
|
||||
## among other places
|
||||
|
||||
def setUp(self):
|
||||
super(UtilTest, self).setUp()
|
||||
warnings.filterwarnings("ignore",
|
||||
"the 'passlib.win32' module is deprecated")
|
||||
|
||||
def test_lmhash(self):
|
||||
from passlib.win32 import raw_lmhash
|
||||
for secret, hash in [
|
||||
("OLDPASSWORD", u("c9b81d939d6fd80cd408e6b105741864")),
|
||||
("NEWPASSWORD", u('09eeab5aa415d6e4d408e6b105741864')),
|
||||
("welcome", u("c23413a8a1e7665faad3b435b51404ee")),
|
||||
]:
|
||||
result = raw_lmhash(secret, hex=True)
|
||||
self.assertEqual(result, hash)
|
||||
|
||||
def test_nthash(self):
|
||||
warnings.filterwarnings("ignore",
|
||||
r"nthash\.raw_nthash\(\) is deprecated")
|
||||
from passlib.win32 import raw_nthash
|
||||
for secret, hash in [
|
||||
("OLDPASSWORD", u("6677b2c394311355b54f25eec5bfacf5")),
|
||||
("NEWPASSWORD", u("256781a62031289d3c2c98c14f1efc8c")),
|
||||
]:
|
||||
result = raw_nthash(secret, hex=True)
|
||||
self.assertEqual(result, hash)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
@@ -0,0 +1,83 @@
|
||||
"""passlib.tests.tox_support - helper script for tox tests"""
|
||||
#=============================================================================
|
||||
# init script env
|
||||
#=============================================================================
|
||||
import os, sys
|
||||
root_dir = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
|
||||
sys.path.insert(0, root_dir)
|
||||
|
||||
#=============================================================================
|
||||
# imports
|
||||
#=============================================================================
|
||||
# core
|
||||
import re
|
||||
import logging; log = logging.getLogger(__name__)
|
||||
# site
|
||||
# pkg
|
||||
from passlib.utils.compat import print_
|
||||
# local
|
||||
__all__ = [
|
||||
]
|
||||
|
||||
#=============================================================================
|
||||
# main
|
||||
#=============================================================================
|
||||
TH_PATH = "passlib.tests.test_handlers"
|
||||
|
||||
def do_hash_tests(*args):
|
||||
"""return list of hash algorithm tests that match regexes"""
|
||||
if not args:
|
||||
print(TH_PATH)
|
||||
return
|
||||
suffix = ''
|
||||
args = list(args)
|
||||
while True:
|
||||
if args[0] == "--method":
|
||||
suffix = '.' + args[1]
|
||||
del args[:2]
|
||||
else:
|
||||
break
|
||||
from passlib.tests import test_handlers
|
||||
names = [TH_PATH + ":" + name + suffix for name in dir(test_handlers)
|
||||
if not name.startswith("_") and any(re.match(arg,name) for arg in args)]
|
||||
print_("\n".join(names))
|
||||
return not names
|
||||
|
||||
def do_preset_tests(name):
|
||||
"""return list of preset test names"""
|
||||
if name == "django" or name == "django-hashes":
|
||||
do_hash_tests("django_.*_test", "hex_md5_test")
|
||||
if name == "django":
|
||||
print_("passlib.tests.test_ext_django")
|
||||
else:
|
||||
raise ValueError("unknown name: %r" % name)
|
||||
|
||||
def do_setup_gae(path, runtime):
|
||||
"""write fake GAE ``app.yaml`` to current directory so nosegae will work"""
|
||||
from passlib.tests.utils import set_file
|
||||
set_file(os.path.join(path, "app.yaml"), """\
|
||||
application: fake-app
|
||||
version: 2
|
||||
runtime: %s
|
||||
api_version: 1
|
||||
threadsafe: no
|
||||
|
||||
handlers:
|
||||
- url: /.*
|
||||
script: dummy.py
|
||||
|
||||
libraries:
|
||||
- name: django
|
||||
version: "latest"
|
||||
""" % runtime)
|
||||
|
||||
def main(cmd, *args):
|
||||
return globals()["do_" + cmd](*args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main(*sys.argv[1:]) or 0)
|
||||
|
||||
#=============================================================================
|
||||
# eof
|
||||
#=============================================================================
|
||||
3621
venv/lib/python3.12/site-packages/passlib/tests/utils.py
Normal file
3621
venv/lib/python3.12/site-packages/passlib/tests/utils.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user