diff options
| author | Eli Collins <elic@assurancetechnologies.com> | 2011-03-23 14:36:19 -0400 |
|---|---|---|
| committer | Eli Collins <elic@assurancetechnologies.com> | 2011-03-23 14:36:19 -0400 |
| commit | ba4e357ba3db8c0fc5ba20ba6a5e7ad8ffbba1ec (patch) | |
| tree | f83a5d0a612aa3ee73523ba13ce3e5b518ba8f75 | |
| parent | 6a96a86a1d16f47b20b65a751f480429dd215cbf (diff) | |
| download | passlib-ba4e357ba3db8c0fc5ba20ba6a5e7ad8ffbba1ec.tar.gz | |
lots of work documenting CryptContext class
| -rw-r--r-- | docs/contents.rst | 2 | ||||
| -rw-r--r-- | docs/lib/passlib.context-interface.rst | 9 | ||||
| -rw-r--r-- | docs/lib/passlib.context-options.rst | 144 | ||||
| -rw-r--r-- | docs/lib/passlib.context.rst | 246 | ||||
| -rw-r--r-- | docs/overview.rst | 32 | ||||
| -rw-r--r-- | passlib/context.py | 237 | ||||
| -rw-r--r-- | passlib/tests/test_context.py | 8 |
7 files changed, 423 insertions, 255 deletions
diff --git a/docs/contents.rst b/docs/contents.rst index 0388691..4912478 100644 --- a/docs/contents.rst +++ b/docs/contents.rst @@ -9,6 +9,8 @@ Table Of Contents overview lib/passlib.context + lib/passlib.context-interface + lib/passlib.context-options lib/passlib.apps lib/passlib.apache lib/passlib.hosts diff --git a/docs/lib/passlib.context-interface.rst b/docs/lib/passlib.context-interface.rst new file mode 100644 index 0000000..e7689b2 --- /dev/null +++ b/docs/lib/passlib.context-interface.rst @@ -0,0 +1,9 @@ +=============================================== +:mod:`passlib.context` - CryptContext interface +=============================================== + +.. currentmodule:: passlib.context + +.. autoclass:: CryptContext(schemes=None, policy=<default policy>, \*\*kwds) + +.. autoclass:: CryptPolicy(\*\*kwds) diff --git a/docs/lib/passlib.context-options.rst b/docs/lib/passlib.context-options.rst new file mode 100644 index 0000000..24ddc72 --- /dev/null +++ b/docs/lib/passlib.context-options.rst @@ -0,0 +1,144 @@ +============================================= +:mod:`passlib.context` - CryptContext options +============================================= + +.. currentmodule:: passlib.context + +Context Configuration Policy +============================ +.. warning:: + + This section's writing and design are still very much in flux. + +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). + +All configuration options are stored in a CryptPolicy object, +which can be created in the following ways: + +* 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) + +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 ``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 +belong to the context, and are treated as if they started with the prefix ``context.``. +The remaining options - + +``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 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.hash_needs_update()`` should return ``False``. + if not specified, none are considered deprecated. + this must be a subset of the names listed in context.schemes + +``context.default`` + the default scheme context should use for generating new hashes. + if not specified, the first entry in ``context.schemes`` is used. + +``context.min_verify_time`` + if specified, all ``context.verify()`` calls will take at least this many seconds. + if set to an amount larger than the time used by the strongest hash in the system, + this prevents an attacker from guessing the strength of particular hashes remotely. + (specified in fractional seconds). + +``{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.hash_needs_update()`` - 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_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_rounds, default_rounds+vary_rounds)``. + * integer value between 0 and 100 with ``%`` suffix - same as above, with integer value equal to ``vary_rounds*default_rounds/100``. + * note that if algorithms indicate they use a logarthmic rounds parameter, the percent syntax equation uses ``log(vary_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/schemes`` keyword, +though they may override the other context 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 + default = sha512_crypt + min_verify_time = 0.1 + + #set some common options for all schemes + all.vary_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 diff --git a/docs/lib/passlib.context.rst b/docs/lib/passlib.context.rst index 138452e..b3af128 100644 --- a/docs/lib/passlib.context.rst +++ b/docs/lib/passlib.context.rst @@ -1,167 +1,83 @@ -============================================= -:mod:`passlib.context` - CryptContext class -============================================= +============================================== +:mod:`passlib.context` - CryptContext Overview +============================================== .. module:: passlib.context - -The :mod:`!passlib.base` module contains a number of core - - -.. autoclass:: CryptContext - -Context Configuration Policy -============================ -.. warning:: - - This section's writing and design are still very much in flux. - -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). - -All configuration options are stored in a CryptPolicy object, -which can be created in the following ways: - -* 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) - -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 ``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 -belong to the context, and are treated as if they started with the prefix ``context.``. -The remaining options - - -``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 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.hash_needs_update()`` should return ``False``. - if not specified, none are considered deprecated. - this must be a subset of the names listed in context.schemes - -``context.default`` - the default scheme context should use for generating new hashes. - if not specified, the first entry in ``context.schemes`` is used. - -``context.min_verify_time`` - if specified, all ``context.verify()`` calls will take at least this many seconds. - if set to an amount larger than the time used by the strongest hash in the system, - this prevents an attacker from guessing the strength of particular hashes remotely. - (specified in fractional seconds). - -``{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.hash_needs_update()`` - 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_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_rounds, default_rounds+vary_rounds)``. - * integer value between 0 and 100 with ``%`` suffix - same as above, with integer value equal to ``vary_rounds*default_rounds/100``. - * note that if algorithms indicate they use a logarthmic rounds parameter, the percent syntax equation uses ``log(vary_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/schemes`` keyword, -though they may override the other context 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 - default = sha512_crypt - min_verify_time = 0.1 - - #set some common options for all schemes - all.vary_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. - - Policy objects can be constructed by the following methods: - - .. automethod:: from_path - .. automethod:: from_string - .. automethod:: from_source - .. automethod:: from_sources - - .. method:: (constructor) - - 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. + :synopsis: CryptContext class for managing multiple password hash schemes + +Overview +======== +Different storage contexts (eg: linux shadow files vs openbsd shadow files) +may use different sets and subsets of the available algorithms. +Similarly, over time, applications may need to deprecate password schemes +in favor of newer ones, or raise the number of rounds required +by existing hashes. + +This module provides the :class:`!CryptContext` class, which is designed +to handle (as much as possible) of these tasks for an application. +Essentially, a :class:`!CryptContext` instance contains a list +of hash handlers that it should recognize, along with information +about which ones are deprecated, which is the default, +and what configuration constraints an application has placed +on a particular hash. + +Since this class contains so many methods and options, +the documentation for this module is broken up into three +sections: + +* Usage examples (below) +* Next, documentation of the complete :doc:`CryptContext interface <passlib.context-interface>`. +* Finally, a comprehensive list of :doc:`CryptContext options <passlib.context-options>`. + +Usage +===== +To start off with a simple example:: + + >>> from passlib.context import CryptContext + + >>> #create a new context that only understands Md5Crypt & DesCrypt: + >>> myctx = CryptContext([ "md5_crypt", "des_crypt" ]) + + >>> #unless overidden, the first hash listed + >>> #will be used as the default for encrypting + >>> #(in this case, md5_crypt): + >>> hash1 = myctx.encrypt("too many secrets") + >>> hash1 + '$1$nH3CrcVr$pyYzik1UYyiZ4Bvl1uCtb.' + + >>> #the scheme may be forced explicitly, + >>> #though it must be one of the ones recognized by the context: + >>> hash2 = myctx.encrypt("too many secrets", scheme="des-crypt") + >>> hash2 + 'm9pvLj4.hWxJU' + + >>> #verification will autodetect the correct type of hash: + >>> myctx.verify("too many secrets", hash1) + True + >>> myctx.verify("too many secrets", hash2) + True + >>> myctx.verify("too many socks", hash2) + False + + >>> #you can also have it identify the algorithm in use: + >>> myctx.identify(hash1) + 'md5_crypt' + + >>> #or just return the handler instance directly: + >>> myctx.identify(hash1, resolve=True) + <class 'passlib.handlers.md5_crypt.md5_crypt'> + +All of the configuration options for a :class:`!CryptContext` instance +are stored in a :class:`CryptPolicy` instance accessible through +their ``policy`` attribute:: + + >>> from passlib.context import CryptContext + >>> myctx = CryptContext([ "md5_crypt", "des_crypt" ], deprecated="des_crypt") + + >>> #get a list of schemes recognized in this context: + >>> myctx.policy.schemes() + [ 'md5-crypt', 'bcrypt' ] + + >>> #get the default handler class : + >>> myctx.policy.get_handler() + <class 'passlib.handlers.md5_crypt.md5_crypt'> diff --git a/docs/overview.rst b/docs/overview.rst index bfcdb4e..d72f094 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -45,19 +45,17 @@ As a quick example of how a password hash can be used directly:: Password Contexts ================= -Direct support of various password schemes is generally not sufficient -for a mature application, which will frequently have to deal with -tables of existing password hashes. These tables may contain -deprecated schemes, hashes using strong schemes but weak individual configurations, -and other border cases. - -PassLib provides an advanced support framework, based around -the :doc:`CryptContext <lib/passlib.context>` class, which takes care of -many of these issues. Each :class:`!CryptContext` instance can be configured -with a list of known hashes, as well as configuration of policy requirements -such as which hash is the default, which ones are deprecated, and other features. - -Building on this, PassLib provides a number of pre-configured :class:`!CryptContext` instances +Mature applications frequently have to deal with tables of existing password +hashes. Over time, they have to migrate to newer and stronger schemes; as well as raise +the requirements for existing algorithms as more processing power becomes available. +In this case, directly importing and handling the various schemes +generally becomes complicated and tedious. For these and similar use-cases, +the :mod:`passlib.context` module provides the :class:`!CryptContext` class, which handles +multiple password hash schemes, deprecation of old hashes, and +many other policy requirements. + +In addition to using the class itself, PassLib provides a number of +pre-configured :class:`!CryptContext` instances in order to get users started quickly: * The :mod:`passlib.apache` module contains classes @@ -70,6 +68,10 @@ in order to get users started quickly: instances for managing hashes as found in the /etc/shadow files on Linux and BSD systems. +* And finally the :mod:`passlib.context` module, which provides + the :class:`!CryptContext` class itself, allowing + an application to setup the particular configuration it required. + .. note:: For new applications which just need drop-in support for some manner @@ -100,3 +102,7 @@ The :mod:`passlib.utils` module contains a large number of support functions, most of which are only needed when are implementing custom password hash schemes. Most users of passlib will not need to use this subpackage. + +.. todo:: + + Add documentation showing how to create custom password hash handlers. diff --git a/passlib/context.py b/passlib/context.py index ccced9a..6c19fb0 100644 --- a/passlib/context.py +++ b/passlib/context.py @@ -93,6 +93,34 @@ class CryptPolicy(object): .. note:: Instances of CryptPolicy should be treated as immutable. + + Policy objects can be constructed by the following methods: + + .. method:: (constructor) + + 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. + + .. automethod:: from_path + .. automethod:: from_string + .. automethod:: from_source + .. automethod:: from_sources + .. automethod:: replace + + .. automethod:: has_schemes + .. automethod:: schemes + .. automethod:: iter_handlers + .. automethod:: get_handler + .. automethod:: get_options + .. automethod:: handler_is_deprecated + .. automethod:: get_min_verify_time + + .. automethod:: iter_config + .. automethod:: to_dict + .. automethod:: to_file + .. automethod:: to_string """ #========================================================= @@ -100,7 +128,15 @@ class CryptPolicy(object): #========================================================= @classmethod def from_path(cls, path, section="passlib"): - "create new policy from specified section of an ini file" + """create new policy from specified section of an ini file. + + :arg path: path to ini file + :param section: option name of section to read from. + + :raises EnvironmentError: if the file cannot be read + + :returns: new CryptPolicy instance. + """ p = ConfigParser() if not p.read([path]): raise EnvironmentError, "failed to read config file" @@ -108,6 +144,13 @@ class CryptPolicy(object): @classmethod def from_string(cls, source, section="passlib"): + """create new policy from specified section of an ini-formatted string. + + :arg source: string containing ini-formatted content. + :param section: option name of section to read from. + + :returns: new CryptPolicy instance. + """ p = ConfigParser() b = StringIO(source) p.readfp(b) @@ -115,7 +158,17 @@ class CryptPolicy(object): @classmethod def from_source(cls, source): - "helper which accepts CryptPolicy, filepath, raw string, and returns policy" + """create new policy from input. + + :arg source: + source may be a dict, CryptPolicy instance, filepath, or raw string. + + the exact type will be autodetected, and the appropriate constructor called. + + :raises TypeError: if source cannot be identified. + + :returns: new CryptPolicy instance. + """ if isinstance(source, cls): #NOTE: can just return source unchanged, #since we're treating CryptPolicy objects as read-only @@ -136,7 +189,17 @@ class CryptPolicy(object): @classmethod def from_sources(cls, sources): - "create new policy from list of existing policy objects" + """create new policy from list of existing policy objects. + + this method takes multiple sources and composites them on top + of eachother, returning a single resulting CryptPolicy instance. + this allows default policies to be specified, and then overridden + on a per-context basis. + + :arg sources: list of sources to build policy from, elements may be any type accepted by :meth:`from_source`. + + :returns: new CryptPolicy instance + """ #check for no sources - should we return blank policy in that case? if len(sources) == 0: #XXX: er, would returning an empty policy be the right thing here? @@ -156,7 +219,19 @@ class CryptPolicy(object): return cls(**kwds) def replace(self, *args, **kwds): - "return copy of policy, with specified options replaced by new values" + """return copy of policy, with specified options replaced by new values. + + this is essentially a convience wrapper around :meth:`from_sources`, + except that it always inserts the current policy as the first element + in the list. + + this allows easily making minor changes from an existing policy object. + + :param args: optional list of sources as accepted by :meth:`from_sources`. + :param kwds: optional specific options to override in the new policy. + + :returns: new CryptPolicy instance + """ sources = [ self ] if args: sources.extend(args) @@ -289,17 +364,25 @@ class CryptPolicy(object): #========================================================= #public interface (used by CryptContext) #========================================================= - def has_handlers(self): + def has_schemes(self): + "check if policy supported *any* schemes; returns True/False" return len(self._handlers) > 0 def iter_handlers(self): - "iterate through all loaded handlers in policy" + "iterate through handlers for all schemes in policy" return iter(self._handlers) + def schemes(self, resolve=False): + "return list of supported schemes; if resolve=True, returns list of handlers instead" + if resolve: + return list(self._handlers) + else: + return [h.name for h in self._handlers] + def get_handler(self, name=None, category=None, required=False): - """given an algorithm name, return algorithm handler which manages it. + """given the name of a scheme, return handler which manages it. - :arg name: name of algorithm, or ``None`` + :arg name: name of scheme, or ``None`` :param category: optional user category :param required: if ``True``, raises KeyError if name not found, instead of returning ``None``. @@ -328,7 +411,13 @@ class CryptPolicy(object): return None def get_options(self, name, category=None): - "return dict of options attached to specified hash" + """return dict of options for specified scheme + + :arg name: name of scheme, or handler instance itself + :param category: optional user category whose options should be returned + + :returns: dict of options for CryptContext internals which are relevant to this name/category combination. + """ if hasattr(name, "name"): name = name.name @@ -370,7 +459,7 @@ class CryptPolicy(object): return kwds def handler_is_deprecated(self, name, category=None): - "check if algorithm is deprecated according to policy" + "check if scheme is marked as deprecated according to this policy; returns True/False" if hasattr(name, "name"): name = name.name dmap = self._deprecated @@ -382,7 +471,7 @@ class CryptPolicy(object): return False def get_min_verify_time(self, category=None): - "return minimal time verify() should run according to policy" + "return minimal time that verify() should take, according to this policy" mvmap = self._min_verify_time if category in mvmap: return mvmap[category] @@ -464,7 +553,7 @@ class CryptPolicy(object): yield format_key(cat, name, opt), value def to_dict(self, resolve=False): - "return as dictionary of keywords" + "return policy as dictionary of keywords" return dict(self.iter_config(resolve=resolve)) def _write_to_parser(self, parser, section): @@ -480,7 +569,7 @@ class CryptPolicy(object): p.write(stream) def to_string(self, section="passlib"): - "render to INI string" + "render to INI string; inverse of from_string() constructor" b = StringIO() self.to_file(b, section) return b.getvalue() @@ -501,6 +590,7 @@ class CryptPolicy(object): #eoc #========================================================= +#load the default policy instance setup by passlib, which all CryptContexts inherit by default default_policy = CryptPolicy.from_string(resource_string("passlib", "default.cfg")) #========================================================= @@ -509,59 +599,22 @@ default_policy = CryptPolicy.from_string(resource_string("passlib", "default.cfg class CryptContext(object): """Helper for encrypting passwords using different algorithms. - Different storage contexts (eg: linux shadow files vs openbsd shadow files) - may use different sets and subsets of the available algorithms. - This class encapsulates such distinctions: it represents an ordered - list of algorithms, each with a unique name. It contains methods - to verify against existing algorithms in the context, - and still encrypt using new algorithms as they are added. - - Because of all of this, it's basically just a list object. - However, it contains some dictionary-like features - such as looking up algorithms by name, and it's restriction - that no two algorithms in a list share the same name - causes it to act more like an "ordered set" than a list. - - In general use, none of this matters. - The typical use case is as follows:: - - >>> from passlib import hash - >>> #create a new context that only understands Md5Crypt & BCrypt - >>> myctx = hash.CryptContext([ hash.BCrypt, hash.Md5Crypt, ]) - - >>> #the last one in the list will be used as the default for encrypting... - >>> hash1 = myctx.encrypt("too many secrets") - >>> hash1 - '$2a$11$RvViwGZL./LkWfdGKTrgeO4khL/PDXKe0TayeVObQdoew7TFwhNFy' - - >>> #choose algorithm explicitly - >>> hash2 = myctx.encrypt("too many secrets", alg="md5-crypt") - >>> hash2 - '$1$E1g0/BY.$gS9XZ4W2Ea.U7jMueBRVA.' - - >>> #verification will autodetect the right hash - >>> myctx.verify("too many secrets", hash1) - True - >>> myctx.verify("too many secrets", hash2) - True - >>> myctx.verify("too many socks", hash2) - False - - >>> #you can also have it identify the algorithm in use - >>> myctx.identify(hash1) - 'bcrypt' - >>> #or just return the CryptHandler instance directly - >>> myctx.identify(hash1, resolve=True) - <passlib.BCrypt object, name="bcrypt"> - - >>> #you can get a list of algs... - >>> myctx.keys() - [ 'md5-crypt', 'bcrypt' ] - - >>> #and get the CryptHandler object by name - >>> bc = myctx['bcrypt'] - >>> bc - <passlib.BCrypt object, name="bcrypt"> + :param policy: optionally override the default policy CryptContext starts with before options are added. + :param kwds: ``schemes`` and all other keywords are passed to the CryptPolicy constructor. + + .. attribute:: policy + + This exposes the :class:`CryptPolicy` instance + which contains the configuration used by this context object. + + .. automethod:: hash_needs_update + .. automethod:: identify + .. automethod:: encrypt + .. automethod:: verify + .. automethod:: genconfig + .. automethod:: genhash + + .. automethod:: replace """ #=================================================================== #instance attrs @@ -579,7 +632,7 @@ class CryptContext(object): policy = CryptPolicy(**kwds) elif kwds: policy = policy.replace(**kwds) - if not policy.has_handlers(): + if not policy.has_schemes(): raise ValueError, "at least one scheme must be specified" self.policy = policy @@ -589,6 +642,7 @@ class CryptContext(object): return "<CryptContext %0xd schemes=%r>" % (id(self), names) def replace(self, **kwds): + "returns new CryptContext with specified options modified from original; similar to CryptPolicy.replace" return CryptContext(policy=self.policy.replace(**kwds)) #=================================================================== @@ -647,7 +701,21 @@ class CryptContext(object): return settings def hash_needs_update(self, hash, category=None): - """check if hash is allowed by current policy, or if secret should be re-encrypted""" + """check if hash is allowed by current policy, or if secret should be re-encrypted. + + the core of CryptContext's support for hash migration: + + this function takes in a hash string, and checks the scheme, + number of rounds, and other properties against the current policy; + and returns True if the hash is using a deprecated scheme, + or is otherwise outside of the bounds specified by the policy. + if so, the password should be re-encrypted using ``ctx.encrypt(passwd)``. + + :arg hash: existing hash string + :param category: optional user category + + :returns: True/False + """ handler = self.identify(hash, resolve=True, required=True) policy = self.policy @@ -681,13 +749,27 @@ class CryptContext(object): #password hash api proxy methods #=================================================================== def genconfig(self, scheme=None, category=None, **settings): - """Call genconfig() for specified handler""" + """Call genconfig() for specified handler + + This wraps the genconfig() method of the appropriate handler + (using the default if none other is specified). + See the :ref:`password-hash-api` for details. + + The main different between this and calling a handlers' genhash method + directly is that this method will add in any policy-specific + options relevant for the particular hash. + """ handler = self.policy.get_handler(scheme, category, required=True) settings = self._prepare_settings(handler, category, **settings) return handler.genconfig(**settings) def genhash(self, secret, config, scheme=None, category=None, **context): - """Call genhash() for specified handler""" + """Call genhash() for specified handler. + + This wraps the genconfig() method of the appropriate handler + (using the default if none other is specified). + See the :ref:`password-hash-api` for details. + """ #NOTE: this doesn't use category in any way, but accepts it for consistency if scheme: handler = self.policy.get_handler(scheme, required=True) @@ -743,7 +825,7 @@ class CryptContext(object): to guess from the hash string. If no hash string is specified, the last algorithm in the list is used. - :param **kwds: + :param \*\*kwds: All other keyword options are passed to the algorithm's encrypt method. The two most common ones are "keep_salt" and "rounds". @@ -756,14 +838,23 @@ class CryptContext(object): return handler.encrypt(secret, **kwds) def verify(self, secret, hash, scheme=None, category=None, **context): - """verify secret against specified hash + """verify secret against specified hash. + + This identifies the scheme used by the hash (within this context), + and verifies that the specified password matches. + + If the policy specified a min_verify_time, this method + will always take at least that amount of time + (so as to not reveal legacy entries which use a weak hash scheme). :arg secret: - the secret to encrypt + the secret to verify :arg hash: hash string to compare to :param scheme: optional force context to use specfic scheme (must be allowed by context) + + :returns: True/False """ #quick checks if hash is None: diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py index 7da9457..df9b78d 100644 --- a/passlib/tests/test_context.py +++ b/passlib/tests/test_context.py @@ -284,14 +284,14 @@ admin.sha512_crypt.max_rounds = 40000 #========================================================= #reading #========================================================= - def test_10_has_handlers(self): - "test has_handlers() method" + def test_10_has_schemes(self): + "test has_schemes() method" p1 = CryptPolicy(**self.sample_config_1pd) - self.assert_(p1.has_handlers()) + self.assert_(p1.has_schemes()) p3 = CryptPolicy(**self.sample_config_3pd) - self.assert_(not p3.has_handlers()) + self.assert_(not p3.has_schemes()) def test_11_iter_handlers(self): "test iter_handlers() method" |
