diff options
| -rw-r--r-- | docs/lib/passlib.base.rst | 10 | ||||
| -rw-r--r-- | docs/password_hash_api.rst | 18 | ||||
| -rw-r--r-- | passlib/base.py | 221 | ||||
| -rw-r--r-- | passlib/tests/test_base.py | 822 | ||||
| -rw-r--r-- | passlib/tests/utils.py | 150 |
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 #========================================================= |
