summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/lib/passlib.base.rst10
-rw-r--r--docs/password_hash_api.rst18
-rw-r--r--passlib/base.py221
-rw-r--r--passlib/tests/test_base.py822
-rw-r--r--passlib/tests/utils.py150
5 files changed, 593 insertions, 628 deletions
diff --git a/docs/lib/passlib.base.rst b/docs/lib/passlib.base.rst
index 1838b37..14cce6f 100644
--- a/docs/lib/passlib.base.rst
+++ b/docs/lib/passlib.base.rst
@@ -34,7 +34,7 @@ Options for configuring a specific hash take the form of the name of
``{name}.{option}`` (eg ``sha512_crypt.default_rounds``); where ``{name}`` is usually the name of a password hash,
and ``{option}`` is one of the options specified below.
There are a few reserved hash names:
-Any options of the form ``default.{option}`` will be inherited by ALL hashes
+Any options of the form ``all.{option}`` will be inherited by all hashes
if they do not have a ``{hash}.{option}`` value overriding the default.
Any options of the form ``context.{option}`` will be treated as options for the context object itself,
and not for a specified hash. Any options of the form ``{option}`` are taken to implicitly
@@ -54,7 +54,7 @@ The remaining options -
if not specified, none are considered deprecated.
this must be a subset of the names listed in context.schemes
-``context.fallback``
+``context.default``
the default scheme context should use for generating new hashes.
if not specified, the last entry in ``context/schemes`` is used.
@@ -134,11 +134,11 @@ A sample policy file::
#configure what schemes the context supports (note the "context." prefix is implied for these keys)
schemes = md5_crypt, sha512_crypt, bcrypt
deprecated = md5_crypt
- fallback = sha512_crypt
+ default = sha512_crypt
min_verify_time = 0.1
- #set some common options for ALL schemes
- default.vary_default_rounds = 10%
+ #set some common options for all schemes
+ all.vary_default_rounds = 10%
#setup some hash-specific defaults
sha512_crypt.min_rounds = 40000
diff --git a/docs/password_hash_api.rst b/docs/password_hash_api.rst
index 2aae8d2..97a6798 100644
--- a/docs/password_hash_api.rst
+++ b/docs/password_hash_api.rst
@@ -282,7 +282,7 @@ across all handlers in passlib.
For schemes which support a variable number of rounds,
the following attributes are usually exposed
-(applications can test by checking for ``default_rounds``):
+(applications can test by checking for ``getattr(handler,"default_rounds",None)>0``):
.. attribute:: default_rounds
@@ -311,14 +311,22 @@ the following attributes are usually exposed
For schemes which support a salt,
the following attributes are usually exposed
-(applications can test by checking for ``max_salt_chars``):
+(applications can test by checking for ``getattr(handler,"max_salt_chars",None)>0``):
+
+.. attribute:: max_salt_chars
+
+ maximum number of characters which will be *used*
+ if a salt string is provided to :func:`genconfig` or :func:`encrypt`.
+ must be positive integer if salts are supported,
+ may be ``None`` or ``0`` if salts are not supported.
.. attribute:: min_salt_chars
minimum number of characters required in salt string,
if provided to :func:`genconfig` or :func:`encrypt`.
+ must be non-negative integer.
-.. attribute:: max_salt_chars
+.. attribute:: salt_charset
- maximum number of characters which will be *used*
- if a salt string is provided to :func:`genconfig` or :func:`encrypt`.
+ string containing list of all characters which are allowed
+ to be specified in salt parameter. usually `passlib.utils.h64.CHARS`.
diff --git a/passlib/base.py b/passlib/base.py
index 0aa4aab..d810cf5 100644
--- a/passlib/base.py
+++ b/passlib/base.py
@@ -221,11 +221,8 @@ def parse_policy_key(key):
cat, name, opt = key
else:
orig = key
- if '/' in key: #legacy format
- key = key.replace("/",".")
- elif '.' not in key and '__' in key: #lets user specifiy programmatically (since python doesn't allow '.')
+ if '.' not in key and '__' in key: #lets user specifiy programmatically (since python doesn't allow '.')
key = key.replace("__", ".")
- key = key.replace(" ","").replace("\t","") #strip out all whitespace from key
parts = key.split(".")
if len(parts) == 1:
cat = None
@@ -299,7 +296,7 @@ class CryptPolicy(object):
elif isinstance(source, (str,unicode)):
#FIXME: this autodetection makes me uncomfortable...
- if any(c in source for c in "\n\r\t"): #none of these chars should be in filepaths, but should be in config string
+ if any(c in source for c in "\n\r\t") or not source.strip(" \t./\;:"): #none of these chars should be in filepaths, but should be in config string
return cls.from_string(source)
else: #other strings should be filepath
@@ -323,7 +320,7 @@ class CryptPolicy(object):
kwds = {}
for source in sources:
policy = cls.from_source(source)
- kwds.update(policy.iteritems())
+ kwds.update(policy.iter_config(resolve=True))
#build new policy
return cls(**kwds)
@@ -345,8 +342,8 @@ class CryptPolicy(object):
#:list of all handlers, in order they will be checked when identifying (reverse of order specified)
_handlers = None #list of password hash handlers instances.
- #:dict mapping category -> fallback handler for that category
- _fallback = None
+ #:dict mapping category -> default handler for that category
+ _default = None
#:dict mapping category -> set of handler names which are deprecated for that category
_deprecated = None
@@ -421,37 +418,57 @@ class CryptPolicy(object):
handlers.append(handler)
#
- #build _deprecated & _fallback maps
+ #build _deprecated & _default maps
#
dmap = self._deprecated = {}
- fmap = self._fallback = {}
+ fmap = self._default = {}
mvmap = self._min_verify_time = {}
for cat, config in options.iteritems():
kwds = config.pop("context", None)
if not kwds:
continue
+
+ #list of deprecated schemes
deps = kwds.get("deprecated")
if deps:
- for scheme in deps:
- if scheme not in seen:
- raise ValueError, "unspecified scheme in deprecated list: %r" % (scheme,)
+ if handlers:
+ for scheme in deps:
+ if scheme not in seen:
+ raise ValueError, "unspecified scheme in deprecated list: %r" % (scheme,)
dmap[cat] = frozenset(deps)
- fb = kwds.get("fallback")
+
+ #default scheme
+ fb = kwds.get("default")
if fb:
- if fb not in seen:
- raise ValueError, "unspecified scheme set as fallback: %r" % (fb,)
- fmap[cat] = self.get_handler(fb, required=True)
+ if handlers:
+ if hasattr(fb, "name"):
+ fb = fb.name
+ if fb not in seen:
+ raise ValueError, "unspecified scheme set as default: %r" % (fb,)
+ fmap[cat] = self.get_handler(fb, required=True)
+ else:
+ fmap[cat] = fb
+
+ #min verify time
value = kwds.get("min_verify_time")
if value:
mvmap[cat] = value
+ #XXX: error or warning if unknown key found in kwds?
#NOTE: for dmap/fmap/mvmap -
- # if no cat=None value is specified, each has it's own fallbacks,
+ # if no cat=None value is specified, each has it's own defaults,
# (handlers[0] for fmap, set() for dmap, 0 for mvmap)
# but we don't store those in dict since it would complicate policy merge operation
#=========================================================
#public interface (used by CryptContext)
#=========================================================
+ def has_handlers(self):
+ return len(self._handlers) > 0
+
+ def iter_handlers(self):
+ "iterate through all loaded handlers in policy"
+ return iter(self._handlers)
+
def get_handler(self, name=None, category=None, required=False):
"""given an algorithm name, return algorithm handler which manages it.
@@ -469,7 +486,7 @@ class CryptPolicy(object):
if handler.name == name:
return handler
else:
- fmap = self._fallback
+ fmap = self._default
if category in fmap:
return fmap[category]
elif category and None in fmap:
@@ -492,11 +509,15 @@ class CryptPolicy(object):
options = self._options
#start with default values
- kwds = options[None].get("default") or {}
+ kwds = options[None].get("all")
+ if kwds is None:
+ kwds = {}
+ else:
+ kwds = kwds.copy()
#mix in category default values
if category and category in options:
- tmp = options[category].get("default")
+ tmp = options[category].get("all")
if tmp:
kwds.update(tmp)
@@ -513,7 +534,7 @@ class CryptPolicy(object):
return kwds
- def is_deprecated(self, name, category=None):
+ def handler_is_deprecated(self, name, category=None):
"check if algorithm is deprecated according to policy"
if hasattr(name, "name"):
name = name.name
@@ -538,68 +559,64 @@ class CryptPolicy(object):
#=========================================================
#serialization
#=========================================================
- def hashandlers(self):
- return self._handlers > 0
-
- def iterhandlers(self):
- "iterate through all loaded handlers in policy"
- return iter(self._handlers)
+ def iter_config(self, ini=False, resolve=False):
+ """iterate through key/value pairs of policy configuration
- def iteritems(self, format="python"):
- """iterate through keys in policy.
+ :param ini:
+ If ``True``, returns data formatted for insertion
+ into INI file. Keys use ``.`` separator instead of ``__``;
+ list of handlers returned as comma-separated strings.
- :param format:
- format results should be returned in.
- * ``python`` - returns keys with ``__`` separator, and raw values
- * ``tuple`` - returns keys as raw (cat,name,opt) tuple, and raw values
- * ``ini`` - returns keys with ``.`` separator, and strings instead of handler lists
+ :param resolve:
+ If ``True``, returns handler objects instead of handler
+ names where appropriate. Ignored if ``ini=True``.
:returns:
iterator which yeilds (key,value) pairs.
"""
- ini = False
- if format == "tuple":
- def format_key(cat, name, opt):
- return (cat, name, opt)
+ #
+ #prepare formatting functions
+ #
+ if ini:
+ fmt1 = "%s.%s.%s"
+ fmt2 = "%s.%s"
+ def encode_handler(h):
+ return h.name
+ def encode_hlist(hl):
+ return ", ".join(h.name for h in hl)
else:
- if format == "ini":
- fmt1 = "%s.%s.%s"
- fmt2 = "%s.%s"
-
- ini = True
- def h2n(handler):
- return handler.name
- def hlist(handlers):
- return ", ".join(map(h2n, handlers))
-
+ fmt1 = "%s__%s__%s"
+ fmt2 = "%s__%s"
+ if resolve:
+ def encode_handler(h):
+ return h
+ def encode_hlist(hl):
+ return list(hl)
else:
- assert format == "python"
- fmt1 = "%s__%s__%s"
- fmt2 = "%s__%s"
-
- def format_key(cat, name, opt):
- if cat:
- return fmt1 % (cat, name or "context", opt)
- if name:
- return fmt2 % (name, opt)
- return opt
+ def encode_handler(h):
+ return h.name
+ def encode_hlist(hl):
+ return [ h.name for h in hl ]
+
+ def format_key(cat, name, opt):
+ if cat:
+ return fmt1 % (cat, name or "context", opt)
+ if name:
+ return fmt2 % (name, opt)
+ return opt
+ #
+ #run through contents of internal configuration
+ #
value = self._handlers
if value:
- value = value[::-1]
- if ini:
- value = hlist(value)
- yield format_key(None, None, "schemes"), value
+ yield format_key(None, None, "schemes"), encode_hlist(reversed(value))
for cat, value in self._deprecated.iteritems():
- if ini:
- value = hlist(value)
- yield format_key(cat, None, "deprecated"), value
+ yield format_key(cat, None, "deprecated"), encode_hlist(value)
- for cat, value in self._fallback.iteritems():
- if ini:
- value = h2n(value)
- yield format_key(cat, None, "fallback"), value
+ for cat, value in self._default.iteritems():
+ yield format_key(cat, None, "default"), encode_handler(value)
for cat, value in self._min_verify_time.iteritems():
yield format_key(cat, None, "min_verify_time"), value
@@ -611,20 +628,20 @@ class CryptPolicy(object):
value = config[opt]
yield format_key(cat, name, opt), value
- def to_dict(self, format="python"):
+ def to_dict(self, resolve=False):
"return as dictionary of keywords"
- return dict(self.iteritems(format))
+ return dict(self.iter_config(resolve=resolve))
def _write_to_parser(self, parser, section):
"helper for to_string / to_file"
- p.add_section(section)
- for k,v in self.iteritems("ini"):
- p.set(section, k,v)
+ parser.add_section(section)
+ for k,v in self.iter_config(ini=True):
+ parser.set(section, k,v)
def to_string(self, section="passlib"):
"render to INI string"
p = ConfigParser()
- self._write_to_parser(p)
+ self._write_to_parser(p, section)
b = StringIO()
p.write(b)
return b.getvalue()
@@ -636,7 +653,7 @@ class CryptPolicy(object):
if not p.read([path]):
raise EnvironmentError, "failed to read existing file"
p.remove_section(section)
- self._write_to_parser(p)
+ self._write_to_parser(p, section)
fh = file(path, "w")
p.write(fh)
fh.close()
@@ -729,29 +746,29 @@ class CryptContext(object):
if kwds:
policy.append(kwds)
policy = CryptPolicy.from_sources(policy)
- if not policy.hashandlers():
+ if not policy.has_handlers():
raise ValueError, "at least one scheme must be specified"
self.policy = policy
def __repr__(self):
#XXX: *could* have proper repr(), but would have to render policy object options, and it'd be *really* long
- names = [ handler.name for handler in self.policy.iterhandlers() ]
+ names = [ handler.name for handler in self.policy.iter_handlers() ]
return "<CryptContext %0xd schemes=%r>" % (id(self), names)
- def replace(self, *args, **kwds):
- "return CryptContext with new policy which has specified values replaced"
- return CryptContext(policy=self.policy.replace(*args,**kwds))
+ ##def replace(self, *args, **kwds):
+ ## "return CryptContext with new policy which has specified values replaced"
+ ## return CryptContext(policy=self.policy.replace(*args,**kwds))
#===================================================================
#policy adaptation
#===================================================================
- def get_handler(self, name=None, category=None, required=False):
- """given an algorithm name, return CryptHandler instance which manages it.
- if no match is found, returns None.
-
- if name is None, will return default algorithm
- """
- return self.policy.get_handler(name, category, required)
+ ##def get_handler(self, name=None, category=None, required=False):
+ ## """given an algorithm name, return CryptHandler instance which manages it.
+ ## if no match is found, returns None.
+ ##
+ ## if name is None, will return default algorithm
+ ## """
+ ## return self.policy.get_handler(name, category, required)
def norm_handler_settings(self, handler, category=None, **settings):
"normalize settings for handler according to context configuration"
@@ -795,13 +812,13 @@ class CryptContext(object):
return settings
- def is_compliant(self, hash, category=None):
+ def hash_is_compliant(self, hash, category=None):
"""check if hash is allowed by current policy, or if secret should be re-encrypted"""
- handler = self.identify(hash, rethandler=True, required=True)
+ handler = self.identify(hash, resolve=True, required=True)
policy = self.policy
#check if handler has been deprecated
- if policy.is_deprecated(handler, category):
+ if policy.handler_is_deprecated(handler, category):
return True
#get options, and call compliance helper (check things such as rounds, etc)
@@ -829,7 +846,7 @@ class CryptContext(object):
#===================================================================
def genconfig(self, scheme=None, category=None, **settings):
"""Call genconfig() for specified handler"""
- handler = self.get_handler(scheme, category, required=True)
+ handler = self.policy.get_handler(scheme, category, required=True)
settings = self.norm_handler_settings(handler, category, **settings)
return handler.genconfig(**settings)
@@ -837,19 +854,19 @@ class CryptContext(object):
"""Call genhash() for specified handler"""
#NOTE: this doesn't use category in any way, but accepts it for consistency
if scheme:
- handler = self.get_handler(scheme, required=True)
+ handler = self.policy.get_handler(scheme, required=True)
else:
- handler = self.identify(config, rethandler=True, required=True)
+ handler = self.identify(config, resolve=True, required=True)
#XXX: could insert normalization to preferred unicode encoding here
return handler.genhash(secret, config, **context)
- def identify(self, hash, category=None, rethandler=False, required=False):
+ def identify(self, hash, category=None, resolve=False, required=False):
"""Attempt to identify which algorithm hash belongs to w/in this context.
:arg hash:
The hash string to test.
- :param rethandler:
+ :param resolve:
If ``True``, returns the handler itself,
instead of the name of the handler.
@@ -865,9 +882,9 @@ class CryptContext(object):
if required:
raise ValueError, "no hash specified"
return None
- for handler in self.policy.iterhandlers():
+ for handler in self.policy.iter_handlers():
if handler.identify(hash):
- if rethandler:
+ if resolve:
return handler
else:
return handler.name
@@ -896,7 +913,7 @@ class CryptContext(object):
"""
if not self:
raise ValueError, "no algorithms registered"
- handler = self.get_handler(scheme, category, required=True)
+ handler = self.policy.get_handler(scheme, category, required=True)
kwds = self.norm_handler_settings(handler, category, **kwds)
#XXX: could insert normalization to preferred unicode encoding here
return handler.encrypt(secret, **kwds)
@@ -923,9 +940,9 @@ class CryptContext(object):
#locate handler
if scheme:
- handler = self.get_handler(scheme, required=True)
+ handler = self.policy.get_handler(scheme, required=True)
else:
- handler = self.identify(hash, rethandler=True, required=True)
+ handler = self.identify(hash, resolve=True, required=True)
#strip context kwds if scheme doesn't use them
##for k in context.keys():
diff --git a/passlib/tests/test_base.py b/passlib/tests/test_base.py
index 82d6fb3..1275f0d 100644
--- a/passlib/tests/test_base.py
+++ b/passlib/tests/test_base.py
@@ -8,519 +8,401 @@ import hashlib
from logging import getLogger
#site
#pkg
-from passlib.base import CryptContext
-from passlib.tests.utils import TestCase
-from passlib.drivers.md5_crypt import Md5Crypt as AnotherHash
+from passlib import hash
+from passlib.base import CryptContext, CryptPolicy
+from passlib.tests.utils import TestCase, mktemp
+from passlib.drivers.md5_crypt import md5_crypt as AnotherHash
from passlib.tests.test_utils_drivers import UnsaltedHash, SaltedHash
#module
log = getLogger(__name__)
-#
-#FIXME: this unit test does not match the current CryptContext *at all*,
-# and needs to be updated / rewritten to match new system
-#
-
#=========================================================
-#CryptContext
+#
#=========================================================
-class CryptContextTest(TestCase):
- "test CryptContext object's behavior"
+class CryptPolicyTest(TestCase):
+ "test CryptPolicy object"
+
+ #TODO: need to test user categories w/in all this
+ case_prefix = "CryptPolicy"
+
+ #=========================================================
+ #sample crypt policies used for testing
#=========================================================
- #0 constructor
+
+ #-----------------------------------------------------
+ #sample 1 - average config file
+ #-----------------------------------------------------
+ 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_1pd = dict(
+ 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_1pid = {
+ "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_1prd = dict(
+ schemes = [ hash.des_crypt, hash.md5_crypt, hash.bsdi_crypt, hash.sha512_crypt],
+ default = hash.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 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",
+ all__vary_rounds = "10%",
+ 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",
+ all__vary_rounds = "10%",
+ 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,
+ )
+
+ #=========================================================
+ #constructors
#=========================================================
def test_00_constructor(self):
- "test CryptContext constructor using classes"
- #create crypt context
- cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
+ "test CryptPolicy() constructor"
+ policy = CryptPolicy(**self.sample_config_1pd)
+ self.assertEquals(policy.to_dict(), self.sample_config_1pd)
+
+ def test_01_from_path(self):
+ "test CryptPolicy.from_path() constructor"
+ path = mktemp()
+ with file(path, "w") as fh:
+ fh.write(self.sample_config_1s)
+ policy = CryptPolicy.from_path(path)
+ self.assertEquals(policy.to_dict(), self.sample_config_1pd)
+
+ #TODO: test if path missing
+
+ def test_02_from_string(self):
+ "test CryptPolicy.from_string() constructor"
+ policy = CryptPolicy.from_string(self.sample_config_1s)
+ self.assertEquals(policy.to_dict(), self.sample_config_1pd)
+
+ def test_03_from_source(self):
+ "test CryptPolicy.from_source() constructor"
+
+ #pass it a path
+ path = mktemp()
+ with file(path, "w") as fh:
+ fh.write(self.sample_config_1s)
+ policy = CryptPolicy.from_source(path)
+ self.assertEquals(policy.to_dict(), self.sample_config_1pd)
+
+ #pass it a string
+ policy = CryptPolicy.from_source(self.sample_config_1s)
+ self.assertEquals(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.assertEquals(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.assertEquals(policy.to_dict(), self.sample_config_1pd)
+
+ #pass multiple sources
+ path = mktemp()
+ with file(path, "w") as fh:
+ fh.write(self.sample_config_1s)
+ policy = CryptPolicy.from_sources([
+ path,
+ self.sample_config_2s,
+ self.sample_config_3pd,
+ ])
+ self.assertEquals(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.assertEquals(p2.to_dict(), self.sample_config_12pd)
+
+ #check repeating overlay makes no change
+ p2b = p2.replace(**self.sample_config_2pd)
+ self.assertEquals(p2b.to_dict(), self.sample_config_12pd)
+
+ #check overlaying sample 3
+ p3 = p2.replace(self.sample_config_3pd)
+ self.assertEquals(p3.to_dict(), self.sample_config_123pd)
- #parse
- c, b, a = cc._handlers
- self.assertIs(a, UnsaltedHash)
- self.assertIs(b, SaltedHash)
- self.assertIs(c, AnotherHash)
+ #=========================================================
+ #reading
+ #=========================================================
+ def test_10_has_handlers(self):
+ "test has_handlers() method"
- def test_01_constructor(self):
- "test CryptContext constructor using instances"
- #create crypt context
- a = UnsaltedHash
- b = SaltedHash
- c = AnotherHash
- cc = CryptContext([a,b,c])
+ p1 = CryptPolicy(**self.sample_config_1pd)
+ self.assert_(p1.has_handlers())
- #verify elements
- self.assertEquals(list(cc._handlers), [c, b, a])
+ p3 = CryptPolicy(**self.sample_config_3pd)
+ self.assert_(not p3.has_handlers())
- #TODO: test constructor using names
+ def test_11_iter_handlers(self):
+ "test iter_handlers() method"
- #=========================================================
- #1 list getters
- #=========================================================
- ##def test_10_getitem(self):
- ## "test CryptContext.__getitem__[idx]"
- ## #create crypt context
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ##
- ## #verify len
- ## self.assertEquals(len(cc), 3)
- ##
- ## #verify getitem
- ## self.assertEquals(cc[0], a)
- ## self.assertEquals(cc[1], b)
- ## self.assertEquals(cc[2], c)
- ## self.assertEquals(cc[-1], c)
- ## self.assertRaises(IndexError, cc.__getitem__, 3)
-
- ##def test_11_index(self):
- ## "test CryptContext.index(elem)"
- ## #create crypt context
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ## d = AnotherHash()
- ##
- ## self.assertEquals(cc.index(a), 0)
- ## self.assertEquals(cc.index(b), 1)
- ## self.assertEquals(cc.index(c), 2)
- ## self.assertEquals(cc.index(d), -1)
-
- ##def test_12_contains(self):
- ## "test CryptContext.__contains__(elem)"
- ## #create crypt context
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ## d = AnotherHash()
- ##
- ## self.assertEquals(a in cc, True)
- ## self.assertEquals(b in cc, True)
- ## self.assertEquals(c in cc, True)
- ## self.assertEquals(d in cc, False)
+ p1 = CryptPolicy(**self.sample_config_1pd)
+ s = self.sample_config_1prd['schemes'][::-1]
+ self.assertEquals(list(p1.iter_handlers()), s)
+
+ p3 = CryptPolicy(**self.sample_config_3pd)
+ self.assertEquals(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.assertEquals(p12.get_options("bsdi_crypt"),dict(
+ vary_rounds = "10%",
+ min_rounds = 29000,
+ max_rounds = 35000,
+ default_rounds = 31000,
+ ))
+
+ self.assertEquals(p12.get_options("sha512_crypt"),dict(
+ vary_rounds = "10%",
+ min_rounds = 45000,
+ max_rounds = 50000,
+ ))
+
+ def test_14_handler_is_deprecated(self):
+ "test handler_is_deprecated() method"
+ pa = CryptPolicy(**self.sample_config_1pd)
+ pb = pa.replace(deprecated=["des_crypt", "bsdi_crypt"])
+
+ self.assert_(not pa.handler_is_deprecated("des_crypt"))
+ self.assert_(not pa.handler_is_deprecated(hash.bsdi_crypt))
+ self.assert_(not pa.handler_is_deprecated("sha512_crypt"))
+
+ self.assert_(pb.handler_is_deprecated("des_crypt"))
+ self.assert_(pb.handler_is_deprecated(hash.bsdi_crypt))
+ self.assert_(not pb.handler_is_deprecated("sha512_crypt"))
+
+ #TODO: test this.
+ ##def test_gen_min_verify_time(self):
+ ## "test get_min_verify_time() method"
#=========================================================
- #2 list setters
+ #serialization
#=========================================================
- ##def test_20_setitem(self):
- ## "test CryptContext.__setitem__"
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ## d = AnotherHash()
- ## self.assertIsNot(c, d)
- ## e = pwhash.Md5Crypt()
- ##
- ## #check baseline
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## #replace 0 w/ d should raise error (AnotherHash already in list)
- ## self.assertRaises(KeyError, cc.__setitem__, 0, d)
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## #replace 0 w/ e
- ## cc[0] = e
- ## self.assertEquals(list(cc), [e, b, c])
- ##
- ## #replace 2 w/ d
- ## cc[2] = d
- ## self.assertEquals(list(cc), [e, b, d])
- ##
- ## #replace -1 w/ c
- ## cc[-1] = c
- ## self.assertEquals(list(cc), [e, b, c])
- ##
- ## #replace -2 w/ d should raise error
- ## self.assertRaises(KeyError, cc.__setitem__, -2, d)
- ## self.assertEquals(list(cc), [e, b, c])
-
- ##def test_21_append(self):
- ## "test CryptContext.__setitem__"
- ## cc = CryptContext([UnsaltedHash])
- ## a, = cc
- ## b = SaltedHash()
- ## c = AnotherHash()
- ## d = AnotherHash()
- ##
- ## self.assertEquals(list(cc), [a])
- ##
- ## #try append
- ## cc.append(b)
- ## self.assertEquals(list(cc), [a, b])
- ##
- ## #and again
- ## cc.append(c)
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## #try append dup
- ## self.assertRaises(KeyError, cc.append, d)
- ## self.assertEquals(list(cc), [a, b, c])
-
- ##def test_20_insert(self):
- ## "test CryptContext.insert"
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ## d = AnotherHash()
- ## self.assertIsNot(c, d)
- ## e = pwhash.Md5Crypt()
- ## f = pwhash.Sha512Crypt()
- ## g = pwhash.UnixCrypt()
- ##
- ## #check baseline
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## #inserting d at 0 should raise error (AnotherHash already in list)
- ## self.assertRaises(KeyError, cc.insert, 0, d)
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## #insert e at start
- ## cc.insert(0, e)
- ## self.assertEquals(list(cc), [e, a, b, c])
- ##
- ## #insert f at end
- ## cc.insert(-1, f)
- ## self.assertEquals(list(cc), [e, a, b, f, c])
- ##
- ## #insert g at end
- ## cc.insert(5, g)
- ## self.assertEquals(list(cc), [e, a, b, f, c, g])
+ def test_20_iter_config(self):
+ "test iter_config() method"
+ p1 = CryptPolicy(**self.sample_config_1pd)
+ self.assertEquals(dict(p1.iter_config()), self.sample_config_1pd)
+ self.assertEquals(dict(p1.iter_config(resolve=True)), self.sample_config_1prd)
+ self.assertEquals(dict(p1.iter_config(ini=True)), self.sample_config_1pid)
+
+ def test_21_to_dict(self):
+ "test to_dict() method"
+ p1 = CryptPolicy(**self.sample_config_1pd)
+ self.assertEquals(p1.to_dict(), self.sample_config_1pd)
+ self.assertEquals(p1.to_dict(resolve=True), self.sample_config_1prd)
+
+ def test_22_to_string(self):
+ "test to_string() method"
+ pa = CryptPolicy(**self.sample_config_1pd)
+ s = pa.to_string() #NOTE: can't compare string directly, ordering etc may not match
+ pb = CryptPolicy.from_string(s)
+ self.assertEquals(pb.to_dict(), self.sample_config_1pd)
#=========================================================
- #3 list dellers
+ #
#=========================================================
- ##def test_30_remove(self):
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ## d = AnotherHash()
- ## self.assertIsNot(c, d)
- ##
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## self.assertRaises(ValueError, cc.remove, d)
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## cc.remove(a)
- ## self.assertEquals(list(cc), [b, c])
- ##
- ## self.assertRaises(ValueError, cc.remove, a)
- ## self.assertEquals(list(cc), [b, c])
-
- ##def test_31_discard(self):
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ## d = AnotherHash()
- ## self.assertIsNot(c, d)
- ##
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## self.assertEquals(cc.discard(d), False)
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## self.assertEquals(cc.discard(a), True)
- ## self.assertEquals(list(cc), [b, c])
- ##
- ## self.assertEquals(cc.discard(a), False)
- ## self.assertEquals(list(cc), [b, c])
+
+#=========================================================
+#CryptContext
+#=========================================================
+class CryptContextTest(TestCase):
+ "test CryptContext object's behavior"
+ case_prefix = "CryptContext"
#=========================================================
- #4 list composition
+ #constructor
#=========================================================
+ def test_00_constructor(self):
+ "test CryptContext simple constructor"
+ #create crypt context using handlers
+ cc = CryptContext([UnsaltedHash, SaltedHash, hash.md5_crypt])
+ c, b, a = cc.policy.iter_handlers()
+ self.assertIs(a, UnsaltedHash)
+ self.assertIs(b, SaltedHash)
+ self.assertIs(c, hash.md5_crypt)
+
+ #create context using names
+ cc = CryptContext([UnsaltedHash, SaltedHash, "md5_crypt"])
+ c, b, a = cc.policy.iter_handlers()
+ self.assertIs(a, UnsaltedHash)
+ self.assertIs(b, SaltedHash)
+ self.assertIs(c, hash.md5_crypt)
- ##def test_40_add(self, lsc=False):
- ## "test CryptContext + list"
- ## #build and join cc to list
- ## a = UnsaltedHash()
- ## b = SaltedHash()
- ## c = AnotherHash()
- ## cc = CryptContext([a, b, c])
- ## ls = [pwhash.Md5Crypt, pwhash.Sha512Crypt]
- ## if lsc:
- ## ls = CryptContext(ls)
- ## cc2 = cc + ls
- ##
- ## #verify types
- ## self.assertIsInstance(cc, CryptContext)
- ## self.assertIsInstance(cc2, CryptContext)
- ## self.assertIsInstance(ls, CryptContext if lsc else list)
- ##
- ## #verify elements
- ## self.assertIsNot(cc, ls)
- ## self.assertIsNot(cc, cc2)
- ## self.assertIsNot(ls, cc2)
- ##
- ## #verify cc
- ## a, b, c = cc
- ## self.assertIsInstance(a, UnsaltedHash)
- ## self.assertIsInstance(b, SaltedHash)
- ## self.assertIsInstance(c, AnotherHash)
- ##
- ## #verify ls
- ## d, e = ls
- ## if lsc:
- ## self.assertIsInstance(d, Md5Crypt)
- ## self.assertIsInstance(e, Sha512Crypt)
- ## else:
- ## self.assertIs(d, Md5Crypt)
- ## self.assertIs(e, Sha512Crypt)
- ##
- ## #verify cc2
- ## a2, b2, c2, d2, e2 = cc2
- ## self.assertIs(a2, a)
- ## self.assertIs(b2, b)
- ## self.assertIs(c2, c)
- ## if lsc:
- ## self.assertIs(d2, d)
- ## self.assertIs(e2, e)
- ## else:
- ## self.assertIsInstance(d2, Md5Crypt)
- ## self.assertIsInstance(e2, Sha512Crypt)
-
- ##def test_41_add(self):
- ## "test CryptContext + CryptContext"
- ## self.test_40_add(lsc=True)
-
- ##def test_42_iadd(self, lsc=False):
- ## "test CryptContext += list"
- ## #build and join cc to list
- ## a = UnsaltedHash()
- ## b = SaltedHash()
- ## c = AnotherHash()
- ## cc = CryptContext([a, b, c])
- ## ls = [Md5Crypt, Sha512Crypt]
- ## if lsc:
- ## ls = CryptContext(ls)
- ##
- ## #baseline
- ## self.assertEquals(list(cc), [a, b, c])
- ## self.assertIsInstance(cc, CryptContext)
- ## self.assertIsInstance(ls, CryptContext if lsc else list)
- ## if lsc:
- ## d, e = ls
- ## self.assertIsInstance(d, Md5Crypt)
- ## self.assertIsInstance(e, Sha512Crypt)
- ##
- ## #add
- ## cc += ls
- ##
- ## #verify types
- ## self.assertIsInstance(cc, CryptContext)
- ## self.assertIsInstance(ls, CryptContext if lsc else list)
- ##
- ## #verify elements
- ## self.assertIsNot(cc, ls)
- ##
- ## #verify cc
- ## a2, b2, c2, d2, e2 = cc
- ## self.assertIs(a2, a)
- ## self.assertIs(b2, b)
- ## self.assertIs(c2, c)
- ## if lsc:
- ## self.assertIs(d2, d)
- ## self.assertIs(e2, e)
- ## else:
- ## self.assertIsInstance(d2, Md5Crypt)
- ## self.assertIsInstance(e2, Sha512Crypt)
- ##
- ## #verify ls
- ## d, e = ls
- ## if lsc:
- ## self.assertIsInstance(d, Md5Crypt)
- ## self.assertIsInstance(e, Sha512Crypt)
- ## else:
- ## self.assertIs(d, Md5Crypt)
- ## self.assertIs(e, Sha512Crypt)
-
- ##def test_43_iadd(self):
- ## "test CryptContext += CryptContext"
- ## self.test_42_iadd(lsc=True)
-
- ##def test_44_extend(self):
- ## a = UnsaltedHash()
- ## b = SaltedHash()
- ## c = AnotherHash()
- ## cc = CryptContext([a, b, c])
- ## ls = [Md5Crypt, Sha512Crypt]
- ##
- ## cc.extend(ls)
- ##
- ## a2, b2, c2, d2, e2 = cc
- ## self.assertIs(a2, a)
- ## self.assertIs(b2, b)
- ## self.assertIs(c2, c)
- ## self.assertIsInstance(d2, Md5Crypt)
- ## self.assertIsInstance(e2, Sha512Crypt)
- ##
- ## self.assertRaises(KeyError, cc.extend, [Sha512Crypt ])
- ## self.assertRaises(KeyError, cc.extend, [Sha512Crypt() ])
+ #TODO: test policy & other options
#=========================================================
- #5 basic crypt interface
+ #policy adaptation
#=========================================================
- def test_50_lookup(self):
- "test CryptContext.lookup()"
- cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- c, b, a = cc._handlers
+ #TODO:
+ #norm_handler_settings
+ #hash_is_compliant
- self.assertEquals(cc.lookup('unsalted_example'), a)
- self.assertEquals(cc.lookup('salted_example'), b)
- self.assertEquals(cc.lookup('md5_crypt'), c)
- self.assertEquals(cc.lookup('des_crypt'), None)
-
- ##self.assertEquals(cc.lookup(['unsalted']), a)
- ##self.assertEquals(cc.lookup(['md5_crypt']), None)
- ##self.assertEquals(cc.lookup(['unsalted', 'salted', 'md5_crypt']), b)
+ #=========================================================
+ #identify
+ #=========================================================
+ def test_20_basic(self):
+ "test basic encrypt/identify/verify functionality"
+ handlers = [UnsaltedHash, SaltedHash, AnotherHash]
+ cc = CryptContext(handlers, policy=None)
- #TODO: lookup required=True
+ #run through handlers
+ for crypt in handlers:
+ h = cc.encrypt("test", scheme=crypt.name)
+ self.assertEquals(cc.identify(h), crypt.name)
+ self.assertEquals(cc.identify(h, resolve=True), crypt)
+ self.assert_(cc.verify('test', h))
+ self.assert_(not cc.verify('notest', h))
- def test_51_identify(self):
- "test CryptContext.identify"
- cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- c, b, a = cc._handlers
+ #test default
+ h = cc.encrypt("test")
+ self.assertEquals(cc.identify(h), AnotherHash.name)
- for crypt in (a, b, c):
- h = crypt.encrypt("test")
- self.assertEquals(cc.identify(h), crypt)
- self.assertEquals(cc.identify(h, name=True), crypt.name)
+ def test_21_identify(self):
+ "test identify() border cases"
+ handlers = [UnsaltedHash, SaltedHash, AnotherHash]
+ cc = CryptContext(handlers, policy=None)
- self.assertEquals(cc.identify('$1$232323123$1287319827'), None)
- self.assertEquals(cc.identify('$1$232323123$1287319827'), None)
+ #check unknown hash
+ self.assertEquals(cc.identify('$9$232323123$1287319827'), None)
+ self.assertRaises(ValueError, cc.identify, '$9$232323123$1287319827', required=True)
#make sure "None" is accepted
self.assertEquals(cc.identify(None), None)
+ self.assertRaises(ValueError, cc.identify, None, required=True)
- def test_52_encrypt_and_verify(self):
- "test CryptContext.encrypt & verify"
- cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- c, b, a = cc._handlers
+ def test_22_verify(self):
+ "test verify() scheme kwd"
+ handlers = [UnsaltedHash, SaltedHash, AnotherHash]
+ cc = CryptContext(handlers, policy=None)
- #check encrypt/id/verify pass for all algs
- for crypt in (a, b, c):
- h = cc.encrypt("test", scheme=crypt.name)
- self.assertEquals(cc.identify(h), crypt)
- self.assertEquals(cc.verify('test', h), True)
- self.assertEquals(cc.verify('notest', h), False)
+ h = AnotherHash.encrypt("test")
- #check default alg
- h = cc.encrypt("test")
- self.assertEquals(cc.identify(h), c)
+ #check base verify
+ self.assert_(cc.verify("test", h))
+ self.assert_(not cc.verify("notest", h))
- #check verify using algs
- self.assertEquals(cc.verify('test', h, scheme='md5_crypt'), True)
+ #check verify using right alg
+ self.assert_(cc.verify('test', h, scheme='md5_crypt'))
+ self.assert_(not cc.verify('notest', h, scheme='md5_crypt'))
+
+ #check verify using wrong alg
self.assertRaises(ValueError, cc.verify, 'test', h, scheme='salted_example')
- def test_53_encrypt_salting(self):
- "test CryptContext.encrypt salting options"
- cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- c, b, a = cc._handlers
- self.assert_('salt' in c.setting_kwds)
+ def test_23_verify_empty_hash(self):
+ "test verify() allows hash=None"
+ handlers = [UnsaltedHash, SaltedHash, AnotherHash]
+ cc = CryptContext(handlers, policy=None)
+ self.assert_(not cc.verify("test", None))
+ for handler in handlers:
+ self.assert_(not cc.verify("test", None, scheme=handler.name))
- h = cc.encrypt("test")
- self.assertEquals(cc.identify(h), c)
-
- s = c.parse(h)
- del s['checksum']
- del s['salt']
- h2 = cc.encrypt("test", **s)
- self.assertEquals(cc.identify(h2), c)
- self.assertNotEquals(h2, h)
-
- s = c.parse(h)
- del s['checksum']
- h3 = cc.encrypt("test", **s)
- self.assertEquals(cc.identify(h3), c)
- self.assertEquals(h3, h)
-
- def test_54_verify_empty(self):
- "test CryptContext.verify allows hash=None"
- cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- self.assertEquals(cc.verify('xxx', None), False)
- for crypt in cc._handlers:
- self.assertEquals(cc.verify('xxx', None, scheme=crypt.name), False)
-
-#XXX: haven't decided if this should be part of protocol
-## def test_55_verify_empty_secret(self):
-## "test CryptContext.verify allows secret=None"
-## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
-## h = cc.encrypt("test")
-## self.assertEquals(cc.verify(None,h), False)
-
- #=========================================================
- #6 crypt-enhanced list interface
- #=========================================================
- ##def test_60_getitem(self):
- ## "test CryptContext.__getitem__[algname]"
- ## #create crypt context
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ##
- ## #verify getitem
- ## self.assertEquals(cc['unsalted'], a)
- ## self.assertEquals(cc['salted'], b)
- ## self.assertEquals(cc['sample'], c)
- ## self.assertRaises(KeyError, cc.__getitem__, 'md5_crypt')
-
- ##def test_61_get(self):
- ## "test CryptContext.get(algname)"
- ## #create crypt context
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ##
- ## #verify getitem
- ## self.assertEquals(cc.get('unsalted'), a)
- ## self.assertEquals(cc.get('salted'), b)
- ## self.assertEquals(cc.get('sample'), c)
- ## self.assertEquals(cc.get('md5_crypt'), None)
-
- ##def test_62_index(self):
- ## "test CryptContext.index(algname)"
- ## #create crypt context
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ##
- ## #verify getitem
- ## self.assertEquals(cc.index('unsalted'), 0)
- ## self.assertEquals(cc.index('salted'), 1)
- ## self.assertEquals(cc.index('sample'), 2)
- ## self.assertEquals(cc.index('md5_crypt'), -1)
-
- ##def test_63_contains(self):
- ## "test CryptContext.__contains__(algname)"
- ## #create crypt context
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## self.assertEquals('salted' in cc, True)
- ## self.assertEquals('unsalted' in cc, True)
- ## self.assertEquals('sample' in cc, True)
- ## self.assertEquals('md5_crypt' in cc, False)
-
- ##def test_64_keys(self):
- ## "test CryptContext.keys()"
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## self.assertEquals(cc.keys(), ['unsalted', 'salted', 'sample'])
-
- ##def test_65_remove(self):
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ##
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## self.assertRaises(KeyError, cc.remove, 'md5_crypt')
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## cc.remove('unsalted')
- ## self.assertEquals(list(cc), [b, c])
- ##
- ## self.assertRaises(KeyError, cc.remove, 'unsalted')
- ## self.assertEquals(list(cc), [b, c])
-
- ##def test_66_discard(self):
- ## cc = CryptContext([UnsaltedHash, SaltedHash, AnotherHash])
- ## a, b, c = cc
- ##
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## self.assertEquals(cc.discard('md5_crypt'), False)
- ## self.assertEquals(list(cc), [a, b, c])
- ##
- ## self.assertEquals(cc.discard('unsalted'), True)
- ## self.assertEquals(list(cc), [b, c])
- ##
- ## self.assertEquals(cc.discard('unsalted'), False)
- ## self.assertEquals(list(cc), [b, c])
#=========================================================
#eoc
#=========================================================
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index cad2194..b707bc4 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -3,9 +3,11 @@
#imports
#=========================================================
#core
+import atexit
import logging; log = logging.getLogger(__name__)
import re
import os
+import tempfile
import unittest
import warnings
try:
@@ -197,13 +199,8 @@ class HandlerCase(TestCase):
.. todo::
write directions on how to use this class.
- for now, see examples in places such as test_unix_crypt
+ for now, see examples in test_drivers
"""
- @classproperty
- def __test__(cls):
- #so nose won't auto run *this* cls, but it will for subclasses
- return cls is not HandlerCase
-
#=========================================================
#attrs to be filled in by subclass for testing specific handler
#=========================================================
@@ -240,20 +237,6 @@ class HandlerCase(TestCase):
]
#=========================================================
- #
- #=========================================================
-
- #optional prefix to prepend to name of test method as it's called,
- #useful when multiple handler test classes being run.
- #default behavior should be sufficient
- def case_prefix(self):
- name = self.handler.name if self.handler else self.__class__.__name__
- backend = getattr(self.handler, "get_backend", None) #set by some of the builtin handlers
- if backend:
- name += " (%s backend)" % (backend(),)
- return name
-
- #=========================================================
#alg interface helpers - allows subclass to overide how
# default tests invoke the handler (eg for context_kwds)
#=========================================================
@@ -285,6 +268,32 @@ class HandlerCase(TestCase):
return 'x' + secret
#=========================================================
+ #internal class attrs
+ #=========================================================
+ @classproperty
+ def __test__(cls):
+ #so nose won't auto run *this* cls, but it will for subclasses
+ return cls is not HandlerCase
+
+ #optional prefix to prepend to name of test method as it's called,
+ #useful when multiple handler test classes being run.
+ #default behavior should be sufficient
+ def case_prefix(self):
+ name = self.handler.name if self.handler else self.__class__.__name__
+ backend = getattr(self.handler, "get_backend", None) #set by some of the builtin handlers
+ if backend:
+ name += " (%s backend)" % (backend(),)
+ return name
+
+ @classproperty
+ def has_salt_info(cls):
+ return 'salt' in cls.handler.setting_kwds and getattr(cls.handler, "max_salt_chars", None) > 0
+
+ @classproperty
+ def has_rounds_info(cls):
+ return 'rounds' in cls.handler.setting_kwds and getattr(cls.handler, "max_rounds", None) > 0
+
+ #=========================================================
#attributes
#=========================================================
def test_00_required_attributes(self):
@@ -398,7 +407,56 @@ class HandlerCase(TestCase):
#=========================================================
#genconfig()
#=========================================================
- #NOTE: no specific genconfig tests yet, but testing in other cases
+ def test_30_genconfig_salt(self):
+ "test genconfig() generates new salt"
+ if 'salt' not in self.handler.setting_kwds:
+ raise SkipTest
+ c1 = self.do_genconfig()
+ c2 = self.do_genconfig()
+ self.assertNotEquals(c1,c2)
+
+ def test_31_genconfig_minsalt(self):
+ "test genconfig() honors min salt chars"
+ if not self.has_salt_info:
+ raise SkipTest
+ handler = self.handler
+ cs = handler.salt_charset
+ mn = handler.min_salt_chars
+ c1 = self.do_genconfig(salt=cs[0] * mn)
+ if mn > 0:
+ self.assertRaises(ValueError, self.do_genconfig, salt=cs[0]*(mn-1))
+
+ def test_32_genconfig_maxsalt(self):
+ "test genconfig() honors max salt chars"
+ if not self.has_salt_info:
+ raise SkipTest
+ handler = self.handler
+ cs = handler.salt_charset
+ mx = handler.max_salt_chars
+ c1 = self.do_genconfig(salt=cs[0] * mx)
+ c2 = self.do_genconfig(salt=cs[0] * (mx+1))
+ self.assertEquals(c1,c2)
+
+ def test_33_genconfig_saltcharset(self):
+ "test genconfig() honors salt charset"
+ if not self.has_salt_info:
+ raise SkipTest
+ handler = self.handler
+ mx = handler.max_salt_chars
+ mn = handler.min_salt_chars
+ cs = handler.salt_charset
+
+ #make sure all listed chars are accepted
+ for i in xrange(0,len(cs),mx):
+ salt = cs[i:i+mx]
+ if len(salt) < mn:
+ salt = (salt*(mn//len(salt)+1))[:mx]
+ self.do_genconfig(salt=salt)
+
+ #check some that aren't
+ for c in '\x00\xff':
+ if c not in cs:
+ self.assertRaises(ValueError, self.do_genconfig, salt=c*mx)
#=========================================================
#genhash()
@@ -441,7 +499,7 @@ class HandlerCase(TestCase):
#encrypt()
#=========================================================
def test_50_encrypt_plain(self):
- "test plain encrypt()"
+ "test encrypt() basic behavior"
if self.supports_unicode:
secret = u"unic\u00D6de"
else:
@@ -454,36 +512,19 @@ class HandlerCase(TestCase):
"test encrypt() refused secret=None"
self.assertRaises(TypeError, self.do_encrypt, None)
- #=========================================================
- #test salt generation
- #=========================================================
- def test_60_genconfig_salt(self):
- "test genconfig() generates new salts"
- if 'salt' not in self.handler.setting_kwds:
- raise SkipTest
- c1 = self.do_genconfig()
- c2 = self.do_genconfig()
- self.assertNotEquals(c1,c2)
-
- def test_61_encrypt_salt(self):
- "test encrypt() generates new salts"
+ def test_52_encrypt_salt(self):
+ "test encrypt() generates new salt"
if 'salt' not in self.handler.setting_kwds:
raise SkipTest
- if self.known_correct_hashes:
- secret, hash = self.known_correct_hashes[0]
- else:
- _, secret, hash = self.known_correct_configs[0]
- hash2 = self.do_encrypt(secret)
- self.assertNotEquals(hash,hash2)
-
- #TODO: test too-short user-provided salts
- #TODO: test too-long user-provided salts
- #TODO: test invalid char in user-provided salts
+ #test encrypt()
+ h1 = self.do_encrypt("stub")
+ h2 = self.do_encrypt("stub")
+ self.assertNotEquals(h1, h2)
#=========================================================
#test max password size
#=========================================================
- def test_70_secret_chars(self):
+ def test_60_secret_chars(self):
"test secret_chars limit"
sc = self.secret_chars
@@ -558,5 +599,22 @@ def create_backend_case(base_test, name):
return dummy
#=========================================================
+#helper for creating temp files - all cleaned up when prog exits
+#=========================================================
+tmp_files = []
+
+def _clean_tmp_files():
+ for path in tmp_files:
+ if os.path.exists(path):
+ os.remove(path)
+atexit.register(_clean_tmp_files)
+
+def mktemp(*args, **kwds):
+ fd, path = tempfile.mkstemp(*args, **kwds)
+ tmp_files.append(path)
+ os.close(fd)
+ return path
+
+#=========================================================
#EOF
#=========================================================