summaryrefslogtreecommitdiff
path: root/docs/lib/passlib.context-tutorial.rst
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-27 03:07:58 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-27 03:07:58 -0400
commitd835e0680e9b20836252388c765136c5e015a6a1 (patch)
tree35dc42e15fb037c6d23ec996b366e01643f15771 /docs/lib/passlib.context-tutorial.rst
parent8c913e33b59a7f37dd7545ae8e4b0258e12c5dc3 (diff)
downloadpasslib-d835e0680e9b20836252388c765136c5e015a6a1.tar.gz
merged rewrite of CryptContext documentation - (near-) full tutorial and reference
Diffstat (limited to 'docs/lib/passlib.context-tutorial.rst')
-rw-r--r--docs/lib/passlib.context-tutorial.rst550
1 files changed, 550 insertions, 0 deletions
diff --git a/docs/lib/passlib.context-tutorial.rst b/docs/lib/passlib.context-tutorial.rst
new file mode 100644
index 0000000..338cbe8
--- /dev/null
+++ b/docs/lib/passlib.context-tutorial.rst
@@ -0,0 +1,550 @@
+.. index:: CryptContext; overview
+
+.. _context-overview:
+.. _context-tutorial:
+
+=========================================================
+:mod:`passlib.context` - CryptContext Overview & Tutorial
+=========================================================
+
+.. module:: passlib.context
+ :synopsis: CryptContext class, for managing multiple password hash schemes
+
+Overview
+========
+The central class in the :mod:`passlib.context` module is the :class:`!CryptContext` class.
+This class is designed to take care of many of the more frequent
+coding patterns which occur in applications that need to handle multiple
+password hashes at once:
+
+ * identifying the algorithm used by a hash, and then verify a password.
+ * configure the default algorithm, load in support for new algorithms,
+ deprecate old ones, set defaults for time-cost parameters, etc.
+ * migrate hashes / re-hash passwords when an algorithm has been deprecated.
+ * load said configuration from a sysadmin configurable file.
+
+The following sections contain a walkthrough of this class, starting
+with some simple examples, and working up to a complex "full-integration" example.
+
+.. seealso:: The :ref:`CryptContext Reference <context-reference>` document,
+ which lists all the options and methods supported by this class.
+
+.. index:: CryptContext; usage examples
+
+Tutorial / Walkthrough
+======================
+* `Basic Usage`_
+* `Using Default Settings`_
+* `Loading & Saving a CryptContext`_
+* `Deprecation & Hash Migration`_
+* `Full Integration Example`_
+
+.. todo::
+ This tutorial doesn't yet cover the ``vary_rounds`` option,
+ or the :ref:`user-categories` system; and a few other parts
+ could use elaboration.
+
+.. _context-basic-example:
+
+.. rst-class:: emphasized
+
+Basic Usage
+-----------
+At it's base, the :class:`!CryptContext` class is just a list of
+:class:`!PasswordHash` objects, imported by name
+from the :mod:`passlib.hash` module. The following snippet creates
+a new context object which supports three hash algorithms --
+:doc:`sha256_crypt <passlib.hash.sha256_crypt>`,
+:doc:`md5_crypt <passlib.hash.md5_crypt>`, and
+:doc:`des_crypt <passlib.hash.des_crypt>`::
+
+ >>> from passlib.context import CryptContext
+ >>> myctx = CryptContext(schemes=["sha256_crypt", "md5_crypt", "des_crypt"])
+
+This new object exposes a very similar set of methods to the :class:`!PasswordHash`
+interface. Hashing and verifying passwords is equally straightforward::
+
+ >>> # loads first algorithm in the list (sha256_crypt),
+ >>> # generates a new salt, and hashes the password:
+ >>> hash1 = myctx.encrypt("joshua")
+ >>> hash1
+ '$5$rounds=80000$HFEGd1wnFknpibRl$VZqjyYcTenv7CtOf986hxuE0pRaGXnuLXyfb7m9xL69'
+
+ >>> # alternately, you can explicitly pick one of the configured algorithms,
+ >>> # through this is rarely needed in practice:
+ >>> hash2 = myctx.encrypt("letmein", scheme="md5_crypt")
+ >>> hash2
+ '$1$e2nig/AC$stejMS1ek6W0/UogYKFao/'
+
+ >>> # when verifying a password, the algorithm is identified automatically:
+ >>> myctx.verify("socks", hash1)
+ False
+ >>> myctx.verify("joshua", hash1)
+ True
+ >>> myctx.verify("joshua", hash2)
+ False
+
+If not told otherwise, the context object will use the first algorithm listed
+in ``schemes`` when encrypting new hashes. This default can be changed by
+using the ``default`` keyword::
+
+ >>> myctx = CryptContext(schemes=["sha256_crypt", "md5_crypt", "des_crypt"],
+ default="des_crypt")
+ >>> hash = myctx.encrypt("password")
+ >>> hash
+ 'bIwNofDzt1LCY'
+
+ >>> myctx.identify(hash)
+ 'des_crypt'
+
+.. seealso::
+
+ * the :meth:`CryptContext.encrypt`, :meth:`~CryptContext.verify`, and :meth:`~CryptContext.identify` methods.
+ * the :ref:`schemes <context-schemes-option>` and :ref:`default <context-default-option>` constructor options.
+
+.. _context-default-settings-example:
+
+.. rst-class:: emphasized
+
+Using Default Settings
+----------------------
+While encrypting and verifying hashes is useful enough, it's not much
+more than could be done by importing the objects into a list.
+The next feature of the :class:`!CryptContext` class is that it
+can store various customized settings for the different algorithms,
+instead of hardcoding them into each :meth:`!encrypt` call.
+As an example, the :class:`sha256_crypt <passlib.hash.sha256_crypt>`
+algorithm supports a ``rounds`` parameter which defaults to 80000,
+and the :class:`ldap_salted_md5 <passlib.hash.ldap_salted_md5>` algorithm uses
+8-byte salts by default::
+
+ >>> from passlib.context import CryptContext
+ >>> myctx = CryptContext(["sha256_crypt", "ldap_salted_md5"])
+
+ >>> # sha256_crypt using 80000 rounds...
+ >>> myctx.encrypt("password", scheme="sha256_crypt")
+ '$5$rounds=80000$GgU/gwNBs9SaObqs$ohY23/zm.8O0TpkGx5fxk0aeVdFpaeKo9GUkMJ0VrMC'
+ ^^^^^
+
+ >>> # ldap_salted_md5 with an 8 byte salt...
+ >>> myctx.encrypt("password", scheme="ldap_salted_md5")
+ '{SMD5}cIYrPh5f/TeUKg9oghECB5fSeu8='
+ ^^^^^^^^^^
+
+Instead of having to pass ``rounds=91234`` or ``salt_size=16`` every time
+:meth:`encrypt` is called, CryptContext supports setting algorithm-specific
+defaults which will be used every time a CryptContext method is invoked.
+These is done by passing the CryptContext constructor a keyword with the format :samp:`{scheme}__{setting}`::
+
+ >>> # this reconfigures the existing context object so that
+ >>> # sha256_crypt now uses 91234 rounds,
+ >>> # and ldap_salted_md5 will use 16 byte salts:
+ >>> myctx.update(sha256_crypt__default_rounds=91234,
+ ... ldap_salted_md5__salt_size=16)
+
+ >>> # the effect of this can be seen the next time encrypt is called:
+ >>> myctx.encrypt("password", scheme="sha256_crypt")
+ '$5$rounds=91234$GgU/gwNBs9SaObqs$ohY23/zm.8O0TpkGx5fxk0aeVdFpaeKo9GUkMJ0VrMC'
+ ^^^^^
+
+ >>> myctx.encrypt("password", scheme="ldap_salted_md5")
+ '{SMD5}NnQh2S2pjnFxwtMhjbVH59TaG6P0/l/r3RsDwPj/n/M='
+ ^^^^^^^^^^^^^^^^^^^^^
+
+.. seealso::
+
+ * the :meth:`CryptContext.update` method.
+ * the :ref:`default_rounds <context-default-rounds-option>` and
+ :ref:`per-scheme setting <context-other-option>` constructor options.
+
+.. _context-serialization-example:
+
+.. rst-class:: emphasized
+
+Loading & Saving a CryptContext
+-------------------------------
+The previous example built up a :class:`!CryptContext` instance
+in two stages, first by calling the constructor, and then the :meth:`update`
+method to make some additional changes. The same configuration
+could of course be done in one step::
+
+ >>> from passlib.context import CryptContext
+ >>> myctx = CryptContext(schemes=["sha256_crypt", "ldap_salted_md5"],
+ ... sha256_crypt__default_rounds=91234,
+ ... ldap_salted_md5__salt_size=16)
+
+This is not much more useful, since these settings still have to be
+hardcoded somewhere in the application. This is where the CryptContext's
+serialization abilities come into play. As a starting point,
+every CryptContext object can dump it's configuration as a dictionary
+suitable for passing back into it's constructor::
+
+ >>> myctx.to_dict()
+ {'schemes': ['sha256_crypt', 'ldap_salted_md5'],
+ 'ldap_salted_md5__salt_size': 16,
+ 'sha256_crypt__default_rounds': 91234}
+
+However, this has been taken a step further, as CryptContext objects
+can also dump their configuration into a `ConfigParser <http://docs.python.org/library/configparser.html>`_-compatible
+string, allowing the configuration to be written to a file::
+
+ >>> cfg = print myctx.to_string()
+ >>> print cfg
+ [passlib]
+ schemes = sha256_crypt, ldap_salted_md5
+ ldap_salted_md5__salt_size = 16
+ sha256_crypt__default_rounds = 912345
+
+This "INI" format consists of a section named ``"[passlib]"``,
+following by key/value pairs which correspond exactly to the CryptContext
+constructor keywords (Keywords which accepts lists of names (such as ``schemes``)
+are automatically converted to/from a comma-separated string)
+This format allows CryptContext configurations to be created
+in a separate file (say as part of an application's larger config file),
+and loaded into the CryptContext at runtime. Such strings can be
+loaded directly when creating the context object::
+
+ >>> # using the special from_string() constructor to
+ >>> # load the exported configuration created in the previous step:
+ >>> myctx2 = CryptContext.from_string(cfg)
+
+ >>> # or it can be loaded from a local file:
+ >>> myctx3 = CryptContext.from_path("/some/path/on/local/system")
+
+This allows applications to completely extract their password hashing
+policies from the code, and into a configuration file with other security settings.
+
+.. note::
+
+ For CryptContext instances which already exist,
+ the :meth:`~CryptContext.load` and :meth:`~CryptContext.load_path`
+ methods can be used to replace the existing state.
+
+.. seealso::
+
+ * the :meth:`~CryptContext.to_dict` and :meth:`~CryptContext.to_string` methods.
+ * the :meth:`CryptContext.from_string` and :meth:`CryptContext.from_path` constructors.
+
+.. _context-migration-example:
+
+.. rst-class:: emphasized
+
+Deprecation & Hash Migration
+----------------------------
+The final and possibly most useful feature of the :class:`CryptContext` class
+is that it can take care of deprecating and migrating existing hashes,
+re-hashing them using the current default algorithm and settings.
+All that is required is that a few settings be added to the configuration,
+and that the application call one extra method whenever a user logs in.
+
+Deprecating Algorithms
+......................
+The first setting that enables the hash migration features is the ``deprecated``
+setting. This should be a list algorithms which are no longer desirable to have
+around, but are included in ``schemes`` to provide legacy support.
+For example::
+
+ >>> # this sets a context that supports 3 algorithms, but considers
+ >>> # two of them (md5_crypt and des_crypt) to be deprecated...
+ >>> from passlib.context import CryptContext
+ >>> myctx = CryptContext(schemes=["sha256_crypt", "md5_crypt", "des_crypt"],
+ deprecated=["md5_crypt", "des_crypt"])
+
+All of the basic methods of this object will behave normally, but after
+an application has verified the user entered the correct password, it can
+check to see if the hash has been deprecated using the
+:meth:`~CryptContext.needs_update` method::
+
+ >>> # assume the user's password was stored as a sha256_crypt hash,
+ >>> # needs_update will show that the hash is still allowed.
+ >>> hash = '$5$rounds=80000$zWZFpsA2egmQY8R9$xp89Vvg1HeDCJ/bTDDN6qkdsCwcMM61vHtM1RNxXur.'
+ >>> myctx.needs_update(hash)
+ False
+
+ >>> # but if the user's password was stored as md5_crypt hash,
+ >>> # need_update will indicate that it is deprecated,
+ >>> # and that the original password needs to be re-hashed...
+ >>> hash = '$1$fmWm78VW$uWjT69xZNMHWyEQjq852d1'
+ >>> myctx.needs_update(hash)
+ True
+
+.. note::
+
+ Internally, this is not the only thing :meth:`!needs_update` does.
+ It also checks for other issues, such as rounds / salts which are
+ known to be weak under certain algorithms, improperly encoded hash
+ strings, and other configurable behaviors that are detailed later.
+
+Integrating Hash Migration
+..........................
+To summarize the process described in the previous section,
+all the actions an application would usually need to
+perform can be combined into the following bit of skeleton code:
+
+.. code-block:: python
+ :linenos:
+
+ hash = get_hash_from_user(user)
+ if pass_ctx.verify(password, hash):
+ if pass_ctx.needs_update(hash):
+ new_hash = pass_ctx.encrypt(password)
+ replace_user_hash(user, new_hash)
+ do_successful_things()
+ else:
+ reject_user_login()
+
+Since this is a very common pattern, the CryptContext object provides
+a shortcut: the :meth:`~CryptContext.verify_and_update` method,
+which allows replacing the above skeleton code with the following
+that uses 2 fewer calls (and is much more efficient internally):
+
+.. code-block:: python
+ :linenos:
+
+ hash = get_hash_from_user(user)
+ valid, new_hash = pass_ctx.verify_and_update(password, hash)
+ if valid:
+ if new_hash:
+ replace_user_hash(user, new_hash)
+ do_successful_things()
+ else:
+ reject_user_login()
+
+.. _context-min-rounds-example:
+
+Settings Rounds Limitations
+...........................
+In addition to deprecating entire algorithms, the deprecations system
+also allows you to place limits on algorithms that support the
+variable time-cost parameter ``rounds``:
+
+As an example, take a typical system containing a number of user passwords,
+all stored using :class:`~passlib.hash.sha256_crypt`.
+As computers get faster, the minimum number of rounds that should be used
+gets larger, yet the existing passwords will remain in the system
+hashed using their original value. To solve this, the CryptContext
+object lets you place minimum bounds on what ``rounds``
+values are allowed, using the :samp:`{scheme}__min_rounds` set of keywords...
+any hashes whose rounds are outside this limit are considered deprecated,
+and in need of re-encoding using the current policy:
+
+First, we set up a context which requires all :class:`!sha256_crypt` hashes
+to have at least 131072 rounds::
+
+ >>> from passlib.context import CryptContext
+ >>> myctx = CryptContext(schemes="sha256_crypt",
+ ... sha256_crypt__min_rounds=131072)
+
+New hashes generated by this context will always honor the minimum
+(just as if ``default_rounds`` was set to the same value)::
+
+ >>> # plain call to encrypt:
+ >>> hash1 = myctx.encrypt("password")
+ '$5$rounds=131072$i6xuFK6j8r66ahGn$r.7H8HUk30qiH7fIWRJFJfhWG925nRZh90aYPMdewr3'
+ ^^^^^^
+ >>> # hashes with enough rounds won't show up as deprecated...
+ >>> myctx.needs_update(hash1)
+ False
+
+Explicitly setting the rounds too low will cause a warning,
+and the minimum will be used anyways::
+
+ >>> # explicit rounds passed to encrypt...
+ >>> myctx.encrypt("password", rounds=1000)
+ __main__:1: PasslibConfigWarning: sha256_crypt config requires rounds >= 131072,
+ increasing value from 80000
+ '$5$rounds=131072$86YrzUF3fGwY99oy$03e/pyh4l3N/G0509er9JiQmIxc0y9lrAJaLswX/iv8'
+ ^^^^^^
+
+But if an existing hash below the minimum is tested, it will show up as needing rehashing::
+
+ >>> # this has only 80000 rounds:
+ >>> hash3 = '$5$rounds=80000$qoCFY.akJr.flB7V$8cIZXLwSTzuCRLcJbgHlxqYKEK0cVCENy6nFIlROj05'
+ >>> myctx.needs_update(hash3)
+ True
+
+ >>> # and verify_and_update() will upgrade this hash automatically:
+ >>> myctx.verify_and_update("wrong", hash3)
+ (False, None)
+ >>> myctx.verify_and_update("password", hash3)
+ (True, '$5$rounds=131072$rnMqBaemVZ6QGu7v$vrAVQLEbsBoxhgem8ynvAbToCae8vpzl6ZuDS3/adlA')
+ ^^^^^^
+
+.. seealso::
+
+ * the :ref:`deprecated <context-deprecated-option>`,
+ :ref:`min_rounds <context-min-rounds-option>`,
+ and :ref:`max_rounds <context-max-rounds-option>` constructor options.
+
+ * the :meth:`~CryptContext.needs_update` and :meth:`~CryptContext.verify_and_update` methods.
+
+.. rst-class:: html-toggle
+
+Full Integration Example
+========================
+The following is an extended example showing how to fully interface
+a CryptContext object into your application. The sample configuration
+is somewhat more ornate that would usually be needed, just to highlight
+some features, but should none-the-less be secure.
+
+Policy Configuration File
+-------------------------
+The first thing to do is setup a configuration string for the CryptContext to use.
+This can be a dictionary or string defined in a python config file,
+or (in this example), part of a large INI-formatted config file.
+All of the documented :ref:`context-options` are allowed.
+
+.. code-block:: ini
+
+ ; the options file uses the INI file format,
+ ; and passlib will only read the section named "passlib",
+ ; so it can be included along with other application configuration.
+
+ [passlib]
+
+ ; setup the context to support pbkdf2_sha256, and some other hashes:
+ schemes = pbkdf2_sha256, sha512_crypt, sha256_crypt, md5_crypt, des_crypt
+
+ ; flag md5_crypt and des_crypt as deprecated
+ deprecated = md5_crypt, des_crypt
+
+ ; set boundaries for the pbkdf2 rounds parameter
+ ; (pbkdf2 hashes outside this range will be flagged as needs-updating)
+ pbkdf2_sha256__min_rounds = 10000
+ pbkdf2_sha256__max_rounds = 50000
+
+ ; set the default rounds to use when encrypting new passwords.
+ ; the 'vary' field will cause each new hash to randomly vary
+ ; from the default by the specified % of the default (in this case,
+ ; 15000 +/- 10% or between 13500 and 16500 rounds).
+ pbkdf2_sha1__default_rounds = 15000
+ pbkdf2_sha1__vary_rounds = 0.1
+
+ ; applications can choose to treat certain user accounts differently,
+ ; by assigning different types of account to a 'user category',
+ ; and setting special policy options for that category.
+ ; this create a category named 'admin', which will have a larger default
+ ; rounds value.
+ admin__pbkdf2_sha1__min_rounds = 18000
+ admin__pbkdf2_sha1__default_rounds = 20000
+
+Initializing the CryptContext
+-----------------------------
+Applications which choose to use a policy file will typically want
+to create the CryptContext at the module level, and then load
+the configuration once the application starts:
+
+1. Within a common module in your application (eg ``myapp.model.security``)::
+
+ #
+ # create a crypt context that can be imported and used wherever is needed...
+ # the instance will be configured later.
+ #
+ from passlib.context import CryptContext
+ user_pwd_context = CryptContext()
+
+2. Within some startup function within your application::
+
+ #
+ # when the app starts, import the context from step 1 and
+ # configure it... such as by loading a policy file (see above)
+ #
+
+ from myapp.model.security import user_pwd_context
+
+ def myapp_startup():
+
+ #
+ # ... other code ...
+ #
+
+ #
+ # load configuration from some application-specified path
+ # using load_path() ... or use the load() method, which can
+ # load a dict or in-memory string containing the INI file.
+ #
+ ##user_pwd_context.load(policy_config_string)
+ user_pwd_context.load_path(policy_config_path)
+
+ #
+ # if you want to reconfigure the context without restarting the application,
+ # simply repeat the above step at another point.
+ #
+
+ #
+ # ... other code ...
+ #
+
+Encrypting New Passwords
+------------------------
+When it comes time to create a new user's password, insert
+the following code in the correct function::
+
+ from myapp.model.security import user_pwd_context
+
+ def handle_user_creation():
+
+ #
+ # ... other code ...
+ #
+
+ # vars:
+ # 'secret' containing the putative password
+ # 'category' containing a category assigned to the user account
+ #
+
+ hash = user_pwd_context.encrypt(secret, category=category)
+
+ #... perform appropriate actions to store hash...
+
+ #
+ # ... other code ...
+ #
+
+.. note::
+
+ In the above code, the 'category' kwd can be omitted entirely, *OR*
+ set to a string matching a user category specified in the policy file.
+ In the latter case, any category-specific policy settings will be enforced.
+ For this example, assume it's ``None`` for most users, and ``"admin"`` for special users.
+ this namespace is entirely application chosen, it just has to match the policy file.
+
+ See :ref:`user-categories` for more details.
+
+Verifying & Migrating Existing Passwords
+----------------------------------------
+Finally, when it comes time to check a users' password, insert
+the following code at the correct place::
+
+ from myapp.model.security import user_pwd_context
+
+ def handle_user_login():
+
+ #
+ # ... other code ...
+ #
+
+ #
+ # this example both checks the user's password AND upgrades deprecated hashes...
+ #
+ # vars:
+ # 'hash' containing the specified user's hash,
+ # 'secret' containing the putative password
+ # 'category' containing a category assigned to the user account
+ #
+
+ ok, new_hash = user_pwd_context.verify_and_update(secret, hash, category=category)
+ if not ok:
+ # ... password did not match. do mean things ...
+ pass
+
+ else:
+ #... password matched ...
+
+ if new_hash:
+ # old hash was deprecated by policy.
+
+ # ... replace hash w/ new_hash for user account ...
+ pass
+
+ # ... do successful login actions ...