1
0
mirror of https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git synced 2025-08-14 00:25:46 +02:00

Initial commit

This commit is contained in:
MoonTestUse1
2024-12-23 19:27:44 +06:00
commit e81df4c87e
4952 changed files with 1705479 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""passlib tests"""

View File

@@ -0,0 +1,6 @@
import os
from nose import run
run(
defaultTest=os.path.dirname(__file__),
)

View File

@@ -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)

View 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
#=============================================================================

View File

@@ -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

View File

@@ -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

Binary file not shown.

View File

@@ -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

View 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
#=============================================================================

View 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
#=============================================================================

File diff suppressed because it is too large Load Diff

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View 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
#=============================================================================

View 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
#=============================================================================

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

View File

@@ -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
#=============================================================================

File diff suppressed because it is too large Load Diff