diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-02-04 18:13:19 -0500 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-02-04 18:13:19 -0500 |
| commit | 691e05128a278ebcbef90159baabfe77344d03a3 (patch) | |
| tree | 7834424d691c68c00e87cececfbba4438fef7bf0 | |
| parent | c6df78f5892122c0a9c3adaa8172cad0e00618cc (diff) | |
| download | passlib-691e05128a278ebcbef90159baabfe77344d03a3.tar.gz | |
major work on policy system
===========================
* added documentation detailing policy system's keys and functionality
* split policy-related code out of CryptContext into CryptPolicy object
* added 'category' kwd to all relevant CryptContext methods
* implemented parsing & introspection methods for CryptPolicy
* added rounds management to CryptContext, per policy specification
* attempt at documenting passlib.unix (incomplete)
TODO
----
* ability to create composite CryptPolicy objects
* per-hash handling of policy compliance checks
* UTs for policy system
| -rw-r--r-- | docs/lib/passlib.base.rst | 162 | ||||
| -rw-r--r-- | docs/lib/passlib.unix.rst | 41 | ||||
| -rw-r--r-- | docs/notes.txt | 10 | ||||
| -rw-r--r-- | passlib/base.py | 539 | ||||
| -rw-r--r-- | passlib/tests/test_hash_sun_md5_crypt.py | 1 | ||||
| -rw-r--r-- | passlib/unix.py | 28 |
6 files changed, 537 insertions, 244 deletions
diff --git a/docs/lib/passlib.base.rst b/docs/lib/passlib.base.rst index aa463f9..688df2d 100644 --- a/docs/lib/passlib.base.rst +++ b/docs/lib/passlib.base.rst @@ -1,8 +1,8 @@ ============================================= -:mod:`passlib` - Crypt Contexts +:mod:`passlib.base` - Crypt Contexts ============================================= -.. currentmodule:: passlib +.. currentmodule:: passlib.base For more complex deployment scenarios than the frontend functions described in :doc:`Quick Start <quickstart>`, @@ -10,26 +10,150 @@ the CryptContext class exists... .. autoclass:: CryptContext -Predefined Contexts -=================== -The following context objects are predefined by BPS: +Context Configuration Policy +============================ +.. warning:: -.. data:: default_context + This section's writing and design are still very much in flux. - This context object contains all the algorithms - supported by BPS, listed (mostly) in order of strength. - :func:`identify`, :func:`verify`, and :func:`encrypt` - are all merely wrappers for this object's methods - of the same name. +Each CryptContext instance is extremely configuration through a wide range +of options. All of these options can be specified via the CryptContext +constructor, or by loading the configuration of a section of an ini file +(allowing an application's password policy to be specified externally). -.. data:: linux_context +All configuration options are stored in a CryptPolicy object, +which can be created in the following ways: - This context object contains only the algorithms - in use on modern linux systems (namely: - unix-crypt, md5-crypt, sha512-crypt). +* passing in options as keywords to it's constructor +* loading options from a section of a :mod:`ConfigParser` ini file. +* compositing together existing CryptPolicy objects (this allows for default policies, application policies, and run-time policies) -.. data:: bsd_context +Hash Configuration Options +========================== +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 +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 +belong to the context, and are treated as if they started with the prefix ``context.``. +The remaining options - - This context object contains only the algorithms - in use on modern BSD systems (namely: - unix-crypt, md5-crypt, bcrypt). +``context.schemes`` + comma separated list of the schemes this context should recognize, specified by name. + when a context is identifying hashes, it will check each scheme in this list + in reverse order. if this value is being specified programmatically, + it may also be a python list containing a mixture of names + and password hash handler objects. + +``context.deprecated`` + comma separated list of the schemes which this context should recognize, + generated hashes only if explicitly requested, and for which ``context.is_compliant()`` should return ``False``. + if not specified, none are considered deprecated. + this must be a subset of the names listed in context.schemes + +``context.fallback`` + the default scheme context should use for generating new hashes. + if not specified, the last entry in ``context/schemes`` is used. + +``{hash}.min_rounds``, ``{hash}.max_rounds`` + + place limits on the number of rounds allowed for a specific hash. + + * these are configurable per-context limits, hard limits set by algorithm are always applied + * if min > max, max will be increased to equal min. + * ``context.genconfig()`` or ``config.encrypt()`` - requests outside of these bounds will be clipped. + * ``context.is_compliant()`` - existing hashes w/ rounds outside of range are not compliant + * for hashes which do not have a rounds parameter, these values are ignored. + +``{hash}.default_rounds`` + + sets the default number of rounds to use when generating new hashes. + + * if this value is out side of per-policy min/max, it will be clipped just like user provided value. + * ``context.genconfig()`` or ``config.encrypt()`` - if rounds are not provided explicitly, this value will be used. + * for hashes which do not have a rounds parameter, this value is ignored. + * if not specified, max_rounds is used if available, then min_rounds, then the algorithm default. + +``{hash}.vary_default_rounds`` + + [only applies if ``{hash}.default_rounds`` is specified and > 0] + + if specified, every time a new hash is created using {hash}/default_rounds for it's rounds value, + the actual value used is generated at random, using default_rounds as a hint. + + * integer value - a value will be chosen using the formula ``randint(default_rounds-vary_default_rounds, default_rounds+vary_default_rounds)``. + * integer value between 0 and 100 with ``%`` suffix - same as above, with integer value equal to ``vary_default_rounds*default_rounds/100``. + * note that if algorithms indicate they use a logarthmic rounds parameter, the percent syntax equation uses ``log(vary_default_rounds*(2**default_rounds)/100,2)``, + to permit a default value to be applicable to all schemes. XXX: this might be a bad / overly complex idea. + +``{hash}.{setting}`` + any keys which match the name of a configuration parameter accepted by the hash + will be used directly as default values. + + * for security purposes, ``salt`` is *forbidden* from being used in this way. + * if ``rounds`` is specified directly, it will override the entire min/max/default_rounds framework. + +``{hash}.{other}`` + any keys which do not fall under the above categories will be ignored + +User Categories +=============== +One frequent need is for certain categories of users (eg the root account) +to have more strigent password requirements than default users. +PassLib allows this by recognizing options of the format ``{category}.{name}.{option}``, +and allowing many of it's entry methods to accept an optional ``category`` parameter. + +When one is specified, any ``{category}.{name}.{option}`` keywords in the configuration +will override any ``{name}.{option}`` keywords. + +In order to simplify behavior and implementation, categories cannot override the ``context/{option}`` keys. + +Default Policies +================ +PassLib defines a library-default policy, updated perodically, providing (hopefully) sensible defaults for the various contexts. +When a new CryptContext is created, a policy is generated from it's constructor arguments, which is then composited +over the library-default policy. You may optionally override the default policy used by overriding the ``policy`` keyword +of CryptContext. This keyword accepts a single CryptPolicy object or string (which will be treated as an ini file to load); +it also accepts a list of CryptPolicys and/or strings, which will be composited together along with any constructor options. + +Sample Policy File +================== +A sample policy file:: + + [passlib] + #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 + + #set some common options for ALL schemes + default.vary_default_rounds = 10% + + #setup some hash-specific defaults + sha512_crypt.min_rounds = 40000 + bcrypt.min_rounds = 10 + + #create a "root" category, which uses bcrypt by default, and has stronger hashes + root.context.fallback = bcrypt + root.sha512_crypt.min_rounds = 100000 + root.bcrypt.min_rounds = 13 + +.. class:: CryptPolicy + + Stores configuration options for a CryptContext object. + + Construction + ------------ + Policy objects can be constructed by the following methods: + + .. automethod:: from_file + + .. method:: CryptPolicy + + You can specify options directly to the constructor. + This accepts dot-seperated keywords such as found in the config file format, + but for programmatic convience, it also accepts keys with ``.`` replaced with ``__``, + allowing options to be specified programmatically in python. diff --git a/docs/lib/passlib.unix.rst b/docs/lib/passlib.unix.rst index 9acf867..254a8f7 100644 --- a/docs/lib/passlib.unix.rst +++ b/docs/lib/passlib.unix.rst @@ -1,9 +1,46 @@ ============================================ -:mod:`passlib.unix` - Password Hash Schemes +:mod:`passlib.unix` - Unix Password Frontend ============================================ .. module:: passlib.unix - :synopsis: helpers for encrypting & verifying passwords on unix systems + :synopsis: frontend for encrypting & verifying passwords on unix systems + +Contexts +======== +This module provides some pre-configured :class:`CryptContext` instances, +tailor to the hashes supported on various unix systems. + +.. object:: linux_context + + this should recognize the hashes used on most linux systems: + :mod:`~passlib.hash.des_crypt`, + :mod:`~passlib.hash.md5_crypt`, + :mod:`~passlib.hash.sha256_crypt`, and + :mod:`~passlib.hash.sha512_crypt` (used as the default). + +.. object:: bsd_context + + this should recognize the hashes used on most bsd systems: + :mod:`~passlib.hash.des_crypt`, + :mod:`~passlib.hash.ext_des_crypt`, + :mod:`~passlib.hash.nthash`, + :mod:`~passlib.hash.md5_crypt`, + :mod:`~passlib.hash.bcrypt` (used as the default). + +.. note:: + + The above contexts will also recognize password hashes + of the form ``!`` or ``*`` as belonging to a special + ``unix-disabled`` handler, whose ``verify()`` method + always returns ``False``. + +Usage +===== + +.. todo:: + + show usage example + .. _modular-crypt-format: diff --git a/docs/notes.txt b/docs/notes.txt index 0ff1376..9137e4d 100644 --- a/docs/notes.txt +++ b/docs/notes.txt @@ -165,16 +165,6 @@ info about upgrade policy scheme, and sun-md5 ref... some sample hashes all using "passwd", including sunmd5 http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9 -nt-hash - md4.new(passwd.encode('utf-16le')).hexdigest().upper() - - mygreatpasswd - CFACF72F5EB60EA15F89E3AF66732545 - - http://www.faqs.org/rfcs/rfc1320.html - - $3$hash - references for hashes & passwords http://cpansearch.perl.org/src/CHANSEN/Authen-Simple-0.4/lib/Authen/Simple/Password.pm http://search.cpan.org/~zefram/Authen-Passphrase-0.007/lib/Authen/Passphrase.pm diff --git a/passlib/base.py b/passlib/base.py index 03d3ad3..7dd8d5b 100644 --- a/passlib/base.py +++ b/passlib/base.py @@ -25,7 +25,7 @@ from warnings import warn #site #libs import passlib.hash as _hmod -from passlib.utils import abstractclassmethod, Undef, is_crypt_handler, splitcomma +from passlib.utils import abstractclassmethod, Undef, is_crypt_handler, splitcomma, rng #pkg #local __all__ = [ @@ -125,29 +125,277 @@ def list_crypt_handlers(): #========================================================= #policy #========================================================= -""" +def parse_policy_key(key): + "helper to normalize & parse policy keys; returns ``(category, name, option)``" + ##if isinstance(k, tuple) and len(k) == 3: + ## cat, name, opt = k + ##else: + orig = k + if '/' in k: #legacy format + k = k.replace("/",".") + elif '.' not in k and '__' in k: #lets user specifiy programmatically (since python doesn't allow '.') + k = k.replace("__", ".") + k = k.replace(" ","").replace("\t","") #strip out all whitespace from key + parts = k.split(".") + if len(parts) == 1: + cat = None + name = "context" + opt, = parts + elif len(parts) == 2: + cat = None + name, opt = parts + elif len(parts) == 3: + cat, name, opt = parts + else: + raise KeyError, "keys must have 0..2 separators: %r" % (orig,) + if cat == "default": + cat = None + assert name + assert opt + return cat, name, opt + +def parse_policy_value(cat, name, opt, value): + "helper to parse policy values" + #FIXME: kinda primitive :| + if name == "context": + if opt == "schemes": + if isinstance(value, str): + return splitcomma(value) + elif opt == "deprecated": + if isinstance(value, str): + return set(splitcomma(value)) + elif isinstance(value, (list,tuple)): + return set(value) + return value + else: + try: + return int(value) + except ValueError: + return value -context file format - +class CryptPolicy(object): + """stores configuration options for a CryptContext object.""" -config ini + #========================================================= + #class methods + #========================================================= + @classmethod + def from_file(cls, path, section="passlib"): + "create new policy from specified section of an ini file" + p = ConfigParser() + if not p.read([path]): + raise EnvironmentError, "failed to read config file" + return cls(**dict(p.items(section))) -[passlib] -allow = des-crypt, md5-crypt, sha256-crypt, sha512-crypt -default = sha512-crypt -deprecate = des-crypt, md5-crypt + @classmethod + def from_sources(cls, sources): + "create new policy from list of existing policy object" + raise NotImplementedError -sha256_crypt/min_rounds = 10000 -sha256_crypt/default_rounds = 40000 + #========================================================= + #instance attrs + #========================================================= -admin/sha256_crypt/default_rounds = 50000 + #:list of all handlers, in order they will be checked when identifying (reverse of order specified) + _handlers = None #list of password hash handlers instances. -CryptContext( - allow=["des-crypt", "md5-crypt", "sha256-crypt", "sha512-crypt"], - default="sha512-crypt", - deprecate=["des-crypt", "md5-crypt"], - sha256_crypt__default_rounds = 40000, -) -""" + #:dict mapping category -> fallback handler for that category + _fallback = None + + #:dict mapping category -> set of handler names which are deprecated for that category + _deprecated = None + + #:dict mapping category -> dict mapping hash name -> dict of options for that hash + # if a category is specified, particular hash names will be mapped ONLY if that category + # has options which differ from the default options. + _options = None + + #========================================================= + #init + #========================================================= + def __init__(self, **kwds): + self._from_dict(**kwds) + + #========================================================= + #internal init helpers + #========================================================= + def _from_dict(self, **kwds): + # + #normalize & sort keywords + # + options = self._options = {None:{"context":{}}} + for k,v in kwds.iteritems(): + cat,name,opt = parse_policy_key(k) + if name == "context": + if cat and opt == "schemes": + raise NotImplementedError, "current code does not support per-category schemes" + #NOTE: forbidding this because it would really complicate the behavior + # of CryptContext.identify & CryptContext.lookup. + # most useful behaviors here can be had by overridding deprecated and default, anyways. + else: + if opt == "salt": + raise KeyError, "'salt' option is not allowed to be set via a policy object" + #NOTE: doing this for security purposes, why would you ever want a fixed salt? + v = parse_policy_value(cat, name, opt, v) + config = options.get(name) + if config is None: + options[name] = {opt:v} + else: + config[opt] = v + + # + #parse list of schemes, and resolve to handlers. + # + handlers = self._handlers = [] + seen = set() + schemes = options[None]['context'].get("schemes") or [] + for scheme in reversed(schemes): #NOTE: reversed() just so last entry is used as default, and is checked first. + #resolve & validate handler + if is_crypt_handler(scheme): + handler = scheme + else: + handler = get_crypt_handler(scheme) + name = handler.name + if not name: + raise KeyError, "handler lacks name: %r" % (handler,) + + #check name hasn't been re-used + if name in seen: + raise KeyError, "multiple handlers with same name: %r" % (name,) + seen.add(name) + + #add to handler list + handlers.append(handler) + + # + #build _deprecated & _fallback maps + # + dmap = self._deprecated = {} + fmap = self._fallback = {} + for cat, config in options.iteritems(): + kwds = config.pop("context", None) + if not kwds: + continue + deps = kwds.get("deprecated") + if deps: + for scheme in deps: + if scheme not in seen: + raise ValueError, "unspecified scheme in deprecated list: %r" % (scheme,) + dmap[cat] = deps + fb = kwds.get("fallback") + if fb: + if fb not in seen: + raise ValueError, "unspecified scheme set as fallback: %r" % (fb,) + fmap[cat] = self.lookup(fb, required=True) + if None not in dmap: + dmap[None] = set() + if None not in fmap and handlers: + fmap[None] = handlers[0] + + #========================================================= + #public interface (used by CryptContext) + #========================================================= + def lookup(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 handler for default scheme + """ + if name: + for handler in self._handlers: + if handler.name == name: + return handler + else: + fmap = self._fallback + if category and category in fmap: + return fmap[category] + if None not in fmap: + raise KeyError, "no crypt algorithms supported" + return fmap[None] + if required: + raise KeyError, "no crypt algorithm by that name: %r" % (name,) + return None + + def get_options(self, name, category=None): + "return dict of options attached to specified hash" + #TODO: pre-calculate or at least cache some of this. + options = self._options + + #start with default values + kwds = options[None].get("default") or {} + + #mix in category default values + if category and category in options: + tmp = options[category].get("default") + if tmp: + kwds.update(tmp) + + #mix in hash-specific options + tmp = options[None].get(name) + if tmp: + kwds.update(tmp) + + #mix in category hash-specific options + if category and category in options: + tmp = options[category].get(name) + if tmp: + kwds.update(tmp) + + return kwds + + def is_deprecated(self, name, category=None): + "check if algorithm is deprecated according to policy" + if hasattr(name, "name"): + name = name.name + dmap = self._deprecated + if category and category in dmap: + return name in dmap[category] + return name in dmap[None] + + #========================================================= + #serialization + #========================================================= + ##def get(self): + ## "get configuration as dictionary" + ## out = {} + ## out['schemes'] =[ + ## handler.name if get_crypt_handler(handler.name,None) is handler else handler + ## for handler in self._handlers + ## ] + ## if self._dset: + ## out['deprecated'] = [h.name for h in self._dset] + ## if self._default is not self._handlers[0]: + ## out['default'] = self._default.name + ## for name, opts in self._config: + ## for k,v in opts.iteritems(): + ## out["%s__%s" % (name, k)] = v + ## return out + ## + ##def write_config_to_file(self, path, section="passlib"): + ## "save context configuration to section of ConfigParser file" + ## p = ConfigParser() + ## if os.path.exists(path): + ## if not p.read([path]): + ## raise EnvironmentError, "failed to read config file" + ## p.remove_section(section) + ## for k,v in self.get_config().items(): + ## if k == "schemes": + ## if any(hasattr(h,"name") for h in v): + ## raise ValueError, "can't write to config file, unregistered handlers in use" + ## if k in ["schemes", "deprecated"]: + ## v = ", ".join(v) + ## k = k.replace("__", "/") + ## p.set(k, v) + ## fh = file(path, "w") + ## p.write(fh) + ## fh.close() + + #========================================================= + #eoc + #========================================================= + + +default_policy = CryptPolicy() #========================================================= # @@ -212,172 +460,44 @@ class CryptContext(object): #=================================================================== #instance attrs #=================================================================== - _handlers = None #list of password hash handlers instances. - _default = None #default handler - _hmap = None #dict mapping handler.name -> handler for all handlers in _handlers - _dset = None #set of all handlers which have been deprecated - _config = None #dict mapping handler.name -> options for norm_handler_settings + policy = None #policy object governing context #=================================================================== #init #=================================================================== - def __init__(self, schemes, **kwds): - self.set_config(schemes, **kwds) + def __init__(self, schemes=None, policy=default_policy, **kwds): + if schemes: + kwds['schemes'] = schemes + if not policy: + policy = CryptPolicy(**kwds) + elif kwds: + tmp = CryptPolicy(**kwds) + if isinstance(policy, list): + policy = CryptPolicy.from_sources(policy + [tmp]) + else: + policy = CryptPolicy.from_sources([policy, tmp]) + if not policy._handlers: + raise ValueError, "at least one scheme must be specified" + self.policy = policy def __repr__(self): - names = [ handler.name for handler in self._handlers ] + names = [ handler.name for handler in self.policy._handlers ] return "CryptContext(%r)" % (names,) #=================================================================== - #setting policy configuration + #policy adaptation #=================================================================== - def _add_scheme(self, scheme): - "helper to add scheme to internal config" - #resolve & validate handler - if is_crypt_handler(scheme): - handler = scheme - else: - handler = get_crypt_handler(scheme) - name = handler.name - if not name: - raise KeyError, "handler lacks name: %r" % (handler,) - - #check name->handler mapping - hmap = self._hmap - other = hmap.get(name) - if other: - if other is handler: #quit if already added to config - return handler - raise KeyError, "multiple handlers with same name: %r" % (handler, other) - hmap[name] = handler - - #add to handler list - self._handlers.append(handler) - - return handler - - def set_config(self, schemes, deprecated=None, default=None, **kwds): - "set configuration from dictionary of options" - - #init handler state - self._handlers = [] - self._hmap = {} - - #parse scheme list - if not schemes: - raise ValueError, "no schemes defined" - if isinstance(schemes, str): - schemes = splitcomma(schemes) - for scheme in reversed(schemes): #NOTE: reversed() just so last entry is used as default, and is checked first. - self._add_scheme(scheme) - - #parse deprecated set - dset = self._dset = set() - if deprecated: - if isinstance(deprecated, str): - deprecated = splitcomma(deprecated) - for scheme in deprecated: - handler = self._add_scheme(scheme) - dset.add(handler) - - #take care of default - if default: - self._default = self._add_scheme(default) - else: - self._default = self._handlers[0] - - #all other keywords should take form "name__param" or "name/param", - #where name is a handler name, and param is a parameter for settings name's policy behavior. - config = self._config = {} - hmap = self._hmap - for key, value in kwds.iteritems(): - if '__' in key: - name, param = key.split("__") - elif '/' in key: - name, param = key.split("/") - else: - raise KeyError, "unknown keyword: %r" % (key,) - if name not in hmap: - raise KeyError, "unknown scheme: %r" % (scheme,) - if name in config: - opts = config[name] - else: - opts = config[name] = {} - opts[param] = value - - def load_config_from_file(self, path, section="passlib"): - "load context configuration from section of ConfigParser file" - p = ConfigParser() - if not p.read([path]): - raise EnvironmentError, "failed to read path" - self.set_config(**dict(p.items(section))) - - #=================================================================== - #exporting policy configuration - #=================================================================== - def get_config(self): - "get configuration as dictionary" - out = {} - out['schemes'] =[ - handler.name if get_crypt_handler(handler.name,None) is handler else handler - for handler in self._handlers - ] - if self._dset: - out['deprecated'] = [h.name for h in self._dset] - if self._default is not self._handlers[0]: - out['default'] = self._default.name - for name, opts in self._config: - for k,v in opts.iteritems(): - out["%s__%s" % (name, k)] = v - return out - - def write_config_to_file(self, path, section="passlib"): - "save context configuration to section of ConfigParser file" - p = ConfigParser() - if os.path.exists(path): - if not p.read([path]): - raise EnvironmentError, "failed to read config file" - p.remove_section(section) - for k,v in self.get_config().items(): - if k == "schemes": - if any(hasattr(h,"name") for h in v): - raise ValueError, "can't write to config file, unregistered handlers in use" - if k in ["schemes", "deprecated"]: - v = ", ".join(v) - k = k.replace("__", "/") - p.set(k, v) - fh = file(path, "w") - p.write(fh) - fh.close() - - #=================================================================== - #examining policy configuratio - #=================================================================== - def lookup(self, name=None, required=False): + def lookup(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 """ - if name and name != "default": - for handler in self._handlers: - if handler.name == name: - return handler - else: - assert self._default - return self._default - if required: - raise KeyError, "no crypt algorithm by that name in context: %r" % (name,) - return None - - def get_handler_settings(self, handler): - "return context-specific default settings for handler or handler name" - return self._config.get(handler.name) or {} + return self.policy.lookup(name, category, required) - def norm_handler_settings(self, handler, **settings): + def norm_handler_settings(self, handler, category=None, **settings): "normalize settings for handler according to context configuration" - #check for config - opts = self._config.get(handler.name) + opts = self.policy.get_options(handler, category) if not opts: return settings @@ -386,29 +506,68 @@ class CryptContext(object): if k not in settings and k in opts: settings[k] = opts[k] - #check context-specified limits + #handle rounds + if 'rounds' in handler.setting_kwds: + #TODO: prep-parse & validate this w/in get_options() ? + mn = opts.get("min_rounds") + mx = opts.get("max_rounds") + rounds = settings.get("rounds") + if rounds is None: + df = opts.get("default_rounds") or mx or mn + if df is not None: + vr = opts.get("vary_default_rounds") + if vr: + if isinstance(vr, str) and vr.endswith("%"): + ##TODO: detect log rounds, and adjust scale + ##vr = int(log(vr*.01*(2**df),2)) + vr = int(df * vr / 100) + rounds = rng.randint(df-vr,df+vr) + else: + rounds = df + if rounds is not None: + if mx and rounds > mx: + rounds = mx + if mn and rounds < mn: #give mn predence if mn > mx + rounds = mn + settings['rounds'] = rounds return settings + def 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, required=True) + policy = self.policy + + #check if handler has been deprecated + if policy.is_deprecated(handler, category): + return True + + #TODO: check specific policy for hash. + #need to work up protocol here. + #probably want to hand off settings to handler. + #or at least check for parse() support, and read 'rounds' parameter. + #options = policy.get_options(handler, category) + return False + #=================================================================== - # + #password hash api proxy methods #=================================================================== - - def genconfig(self, scheme=None, **settings): + def genconfig(self, scheme=None, category=None, **settings): """Call genconfig() for specified handler""" - handler = self.lookup(scheme, required=True) - settings = self.norm_handler_settings(handler, **settings) + handler = self.lookup(scheme, category, required=True) + settings = self.norm_handler_settings(handler, category, **settings) return handler.genconfig(**settings) - def genhash(self, config, scheme=None, **context): + def genhash(self, config, scheme=None, category=None, **context): """Call genhash() for specified handler""" + #NOTE: this doesn't use category in any way, but accepts it for consistency if scheme: handler = self.lookup(scheme, required=True) else: handler = self.identify(config, required=True) return handler.genhash(config, **context) - def identify(self, hash, name=False, required=False): + def identify(self, hash, category=None, name=False, required=False): """Attempt to identify which algorithm hash belongs to w/in this context. :arg hash: @@ -425,6 +584,7 @@ class CryptContext(object): The handler which first identifies the hash, or ``None`` if none of the algorithms identify the hash. """ + #NOTE: this doesn't use category in any way, but accepts it for consistency if hash is None: if required: raise ValueError, "no hash specified" @@ -439,7 +599,7 @@ class CryptContext(object): raise ValueError, "hash could not be identified" return None - def encrypt(self, secret, scheme=None, **kwds): + def encrypt(self, secret, scheme=None, category=None, **kwds): """encrypt secret, returning resulting hash. :arg secret: @@ -460,11 +620,11 @@ class CryptContext(object): """ if not self: raise ValueError, "no algorithms registered" - handler = self.lookup(scheme, required=True) - kwds = self.norm_handler_settings(handler, **kwds) + handler = self.lookup(scheme, category, required=True) + kwds = self.norm_handler_settings(handler, category, **kwds) return handler.encrypt(secret, **kwds) - def verify(self, secret, hash, scheme=None, **context): + def verify(self, secret, hash, scheme=None, category=None, **context): """verify secret against specified hash :arg secret: @@ -495,27 +655,6 @@ class CryptContext(object): return handler.verify(secret, hash, **context) #========================================================= - #policy variants - #========================================================= - def verify_and_update(self, secret, hash, **context): - """verify secret against specified hash, and re-encrypt secret if needed""" - ok = self.verify(secret, hash, **context) - if ok and self.needs_update(hash): - return True, self.encrypt(secret, **context) - else: - return ok, None - - def needs_update(self, hash): - """check if hash is allowed by current policy, or should be re-encrypted""" - handler = self.identify(hash, required=True) - if handler in self._dset: - return True - #TODO: check specific policy for hash. - #need to work up protocol here. - #probably want to hand off settings to handler. - return False - - #========================================================= #eoc #========================================================= diff --git a/passlib/tests/test_hash_sun_md5_crypt.py b/passlib/tests/test_hash_sun_md5_crypt.py index 2cc6f70..56a6e11 100644 --- a/passlib/tests/test_hash_sun_md5_crypt.py +++ b/passlib/tests/test_hash_sun_md5_crypt.py @@ -20,6 +20,7 @@ class SunMd5CryptTest(_HandlerTestCase): handler = mod known_correct = [ + #sample hash found at http://compgroups.net/comp.unix.solaris/password-file-in-linux-and-solaris-8-9 ("passwd", "$md5$RPgLF6IJ$WTvAlUJ7MqH5xak2FMEwS/"), ] diff --git a/passlib/unix.py b/passlib/unix.py index ee1a387..7677d13 100644 --- a/passlib/unix.py +++ b/passlib/unix.py @@ -5,7 +5,7 @@ from passlib.utils.handlers import CryptHandler #========================================================= #helpers #========================================================= -class DisabledHandler(CryptHandler): +class UnixDisabledHandler(CryptHandler): """fake crypt handler which handles special (non-hash) strings found in /etc/shadow unix shadow files sometimes have "!" or "*" characters indicating logins are disabled. @@ -14,7 +14,7 @@ class DisabledHandler(CryptHandler): this is a fake password hash, designed to recognize those values, and return False for all verify attempts. """ - name = "disabled" + name = "unix-disabled" setting_kwds = () context_kwds = () @@ -34,7 +34,7 @@ class DisabledHandler(CryptHandler): def verify(cls, hash): return False -register_crypt_handler(DisabledHandler) +register_crypt_handler(UnixDisabledHandler) #TODO: UnknownCryptHandler - given hash, detect if system crypt recognizes it, # allowing for pass-through for unknown ones. @@ -44,25 +44,27 @@ register_crypt_handler(DisabledHandler) #========================================================= #default context for quick use.. recognizes common algorithms, uses SHA-512 as default -#er... should we promote bcrypt as default -default_context = CryptContext(["disabled", "des_crypt", "md5_crypt", "bcrypt", "sha256_crypt", "sha512_crypt"]) +#er... should we promote bcrypt as default? +default_context = CryptContext(["unix-disabled", "des_crypt", "md5_crypt", "bcrypt", "sha256_crypt", "sha512_crypt"]) #========================================================= #some general os-context helpers (these may not match your os policy exactly, but are generally useful) #========================================================= -#referencing source via -http://fxr.googlebit.com -# freebsd 6,7,8 - des, md5, bcrypt, nthash -# netbsd - des, ext, md5, bcrypt, sha1 (TODO) -# openbsd - des, ext, md5, bcrypt #referencing linux shadow... # linux - des,md5, sha256, sha512 -linux_context = CryptContext([ "disabled", "des_crypt", "md5_crypt", "sha256_crypt", "sha512_crypt" ]) -freebsd_context = CryptContext([ "disabled", "des_crypt", "nthash", "md5_crypt", "bcrypt"]) -openbsd_context = CryptContext([ "disabled", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt"]) -netbsd_context = CryptContext([ "disabled", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt"]) +linux_context = CryptContext([ "unix-disabled", "des_crypt", "md5_crypt", "sha256_crypt", "sha512_crypt" ]) + +#referencing source via -http://fxr.googlebit.com +# freebsd 6,7,8 - des, md5, bcrypt, nthash +# netbsd - des, ext, md5, bcrypt, sha1 (TODO) +# openbsd - des, ext, md5, bcrypt +bsd_context = CryptContext(["unix-disabled", "nthash", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt"]) +freebsd_context = CryptContext([ "unix-disabled", "des_crypt", "nthash", "md5_crypt", "bcrypt"]) +openbsd_context = CryptContext([ "unix-disabled", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt"]) +netbsd_context = CryptContext([ "unix-disabled", "des_crypt", "ext_des_crypt", "md5_crypt", "bcrypt"]) #========================================================= #eof |
