summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-02-04 18:13:19 -0500
committerEli Collins <elic@assurancetechnologies.com>2011-02-04 18:13:19 -0500
commit691e05128a278ebcbef90159baabfe77344d03a3 (patch)
tree7834424d691c68c00e87cececfbba4438fef7bf0
parentc6df78f5892122c0a9c3adaa8172cad0e00618cc (diff)
downloadpasslib-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.rst162
-rw-r--r--docs/lib/passlib.unix.rst41
-rw-r--r--docs/notes.txt10
-rw-r--r--passlib/base.py539
-rw-r--r--passlib/tests/test_hash_sun_md5_crypt.py1
-rw-r--r--passlib/unix.py28
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